xrpld
Loading...
Searching...
No Matches
tests/libxrpl/shamap/SHAMap.cpp
1#include <xrpl/shamap/SHAMap.h>
2
3#include <xrpl/basics/Blob.h>
4#include <xrpl/basics/Buffer.h>
5#include <xrpl/basics/SHAMapHash.h>
6#include <xrpl/basics/Slice.h>
7#include <xrpl/basics/base_uint.h>
8#include <xrpl/beast/utility/Journal.h>
9#include <xrpl/beast/utility/Zero.h>
10#include <xrpl/protocol/Serializer.h>
11#include <xrpl/shamap/SHAMapInnerNode.h>
12#include <xrpl/shamap/SHAMapItem.h>
13#include <xrpl/shamap/SHAMapLeafNode.h>
14#include <xrpl/shamap/SHAMapMissingNode.h>
15#include <xrpl/shamap/SHAMapNodeID.h>
16#include <xrpl/shamap/SHAMapTreeNode.h>
17
18#include <gtest/gtest.h>
19#include <helpers/TestSink.h>
20#include <shamap/common.h>
21
22#include <algorithm>
23#include <array>
24#include <cstddef>
25#include <cstdint>
26#include <memory>
27#include <string>
28#include <string_view>
29#include <type_traits>
30#include <utility>
31#include <vector>
32
33namespace xrpl::tests {
34
35#ifndef __INTELLISENSE__
36static_assert(std::is_nothrow_destructible<SHAMap>{});
37static_assert(!std::is_default_constructible<SHAMap>{});
38static_assert(!std::is_copy_constructible<SHAMap>{});
39static_assert(!std::is_copy_assignable<SHAMap>{});
40static_assert(!std::is_move_constructible<SHAMap>{});
41static_assert(!std::is_move_assignable<SHAMap>{});
42
43static_assert(std::is_nothrow_destructible<SHAMap::ConstIterator>{});
44static_assert(std::is_copy_constructible<SHAMap::ConstIterator>{});
45static_assert(std::is_copy_assignable<SHAMap::ConstIterator>{});
46static_assert(std::is_move_constructible<SHAMap::ConstIterator>{});
47static_assert(std::is_move_assignable<SHAMap::ConstIterator>{});
48
49static_assert(std::is_nothrow_destructible<SHAMapItem>{});
50static_assert(!std::is_default_constructible<SHAMapItem>{});
51static_assert(!std::is_copy_constructible<SHAMapItem>{});
52
53static_assert(std::is_nothrow_destructible<SHAMapNodeID>{});
54static_assert(std::is_default_constructible<SHAMapNodeID>{});
55static_assert(std::is_copy_constructible<SHAMapNodeID>{});
56static_assert(std::is_copy_assignable<SHAMapNodeID>{});
57static_assert(std::is_move_constructible<SHAMapNodeID>{});
58static_assert(std::is_move_assignable<SHAMapNodeID>{});
59
60static_assert(std::is_nothrow_destructible<SHAMapHash>{});
61static_assert(std::is_default_constructible<SHAMapHash>{});
62static_assert(std::is_copy_constructible<SHAMapHash>{});
63static_assert(std::is_copy_assignable<SHAMapHash>{});
64static_assert(std::is_move_constructible<SHAMapHash>{});
65static_assert(std::is_move_assignable<SHAMapHash>{});
66
67static_assert(std::is_nothrow_destructible<SHAMapTreeNode>{});
68static_assert(!std::is_default_constructible<SHAMapTreeNode>{});
69static_assert(!std::is_copy_constructible<SHAMapTreeNode>{});
70static_assert(!std::is_copy_assignable<SHAMapTreeNode>{});
71static_assert(!std::is_move_constructible<SHAMapTreeNode>{});
72static_assert(!std::is_move_assignable<SHAMapTreeNode>{});
73
74static_assert(std::is_nothrow_destructible<SHAMapInnerNode>{});
75static_assert(!std::is_default_constructible<SHAMapInnerNode>{});
76static_assert(!std::is_copy_constructible<SHAMapInnerNode>{});
77static_assert(!std::is_copy_assignable<SHAMapInnerNode>{});
78static_assert(!std::is_move_constructible<SHAMapInnerNode>{});
79static_assert(!std::is_move_assignable<SHAMapInnerNode>{});
80
81static_assert(std::is_nothrow_destructible<SHAMapLeafNode>{});
82static_assert(!std::is_default_constructible<SHAMapLeafNode>{});
83static_assert(!std::is_copy_constructible<SHAMapLeafNode>{});
84static_assert(!std::is_copy_assignable<SHAMapLeafNode>{});
85static_assert(!std::is_move_constructible<SHAMapLeafNode>{});
86static_assert(!std::is_move_assignable<SHAMapLeafNode>{});
87#endif
88
89inline bool
90operator!=(SHAMapItem const& a, SHAMapItem const& b)
91{
92 return a.key() != b.key();
93}
94
100
101constexpr SHAMapBackingMode kBackedMode{.backed = true, .testName = "backed"};
102constexpr SHAMapBackingMode kUnbackedMode{.backed = false, .testName = "unbacked"};
103
105shamapBackingModeName(::testing::TestParamInfo<SHAMapBackingMode> const& info)
106{
107 return std::string{info.param.testName};
108}
109
110class SHAMapTest : public ::testing::TestWithParam<SHAMapBackingMode>
111{
112protected:
114
115 static Buffer
117 {
118 Buffer vuc{32};
119 vuc.fill(v);
120 return vuc;
121 }
122};
123
124TEST_P(SHAMapTest, add_traverse_snapshot_build_tear_and_iterate)
125{
126 auto const testMode = GetParam();
128
129 // kH3 and kH4 differ only in the leaf, same terminal node (level 19)
130 constexpr UInt256 kH1("092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
131 constexpr UInt256 kH2("436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
132 constexpr UInt256 kH3("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
133 constexpr UInt256 kH4("b92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
134
135 SHAMap sMap{SHAMapType::FREE, f};
136 sMap.invariants();
137 if (!testMode.backed)
138 sMap.setUnbacked();
139
140 auto i1 = makeShamapitem(kH1, intToVuc(1));
141 auto i2 = makeShamapitem(kH2, intToVuc(2));
142 auto i3 = makeShamapitem(kH3, intToVuc(3));
143 auto i4 = makeShamapitem(kH4, intToVuc(4));
144
145 EXPECT_TRUE(sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i2))) << "no add";
146 sMap.invariants();
147 EXPECT_TRUE(sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i1))) << "no add";
148 sMap.invariants();
149
150 auto i = sMap.begin();
151 auto e = sMap.end();
152 EXPECT_FALSE(i == e || (*i != *i1)) << "bad traverse";
153 ++i;
154 EXPECT_FALSE(i == e || (*i != *i2)) << "bad traverse";
155 ++i;
156 EXPECT_EQ(i, e) << "bad traverse";
158 sMap.invariants();
159 sMap.delItem(i2->key());
160 sMap.invariants();
162 sMap.invariants();
163 i = sMap.begin();
164 e = sMap.end();
165 EXPECT_FALSE(i == e || (*i != *i1)) << "bad traverse";
166 ++i;
167 EXPECT_FALSE(i == e || (*i != *i3)) << "bad traverse";
168 ++i;
169 EXPECT_FALSE(i == e || (*i != *i4)) << "bad traverse";
170 ++i;
171 EXPECT_EQ(i, e) << "bad traverse";
172
173 SHAMapHash const mapHash = sMap.getHash();
174 std::shared_ptr<SHAMap> const map2 = sMap.snapShot(false);
175 map2->invariants();
176 EXPECT_EQ(sMap.getHash(), mapHash) << "bad snapshot";
177 EXPECT_EQ(map2->getHash(), mapHash) << "bad snapshot";
178
179 SHAMap::Delta delta;
180 ASSERT_TRUE(sMap.compare(*map2, delta, 100));
181 EXPECT_TRUE(delta.empty());
182
183 EXPECT_TRUE(sMap.delItem(sMap.begin()->key())) << "bad mod";
184 sMap.invariants();
185 EXPECT_NE(sMap.getHash(), mapHash) << "bad snapshot";
186 EXPECT_EQ(map2->getHash(), mapHash) << "bad snapshot";
187
188 ASSERT_TRUE(sMap.compare(*map2, delta, 100));
189 ASSERT_EQ(delta.size(), 1);
190 EXPECT_EQ(delta.begin()->first, kH1);
191 EXPECT_EQ(delta.begin()->second.first, nullptr);
192 ASSERT_NE(delta.begin()->second.second, nullptr);
193 EXPECT_EQ(delta.begin()->second.second->key(), kH1);
194
195 sMap.dump();
196 {
197 constexpr std::array kKeys{
198 UInt256{"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
199 UInt256{"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
200 UInt256{"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
201 UInt256{"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
202 UInt256{"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
203 UInt256{"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
204 UInt256{"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
205 UInt256{"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
206 };
207
208 constexpr std::array kHashes{
209 UInt256{"B7387CFEA0465759ADC718E8C42B52D2309D179B326E239EB5075C64B6281F7F"},
210 UInt256{"FBC195A9592A54AB44010274163CB6BA95F497EC5BA0A8831845467FB2ECE266"},
211 UInt256{"4E7D2684B65DFD48937FFB775E20175C43AF0C94066F7D5679F51AE756795B75"},
212 UInt256{"7A2F312EB203695FFD164E038E281839EEF06A1B99BFC263F3CECC6C74F93E07"},
213 UInt256{"395A6691A372387A703FB0F2C6D2C405DAF307D0817F8F0E207596462B0E3A3E"},
214 UInt256{"D044C0A696DE3169CC70AE216A1564D69DE96582865796142CE7D98A84D9DDE4"},
215 UInt256{"76DCC77C4027309B5A91AD164083264D70B77B5E43E08AEDA5EBF94361143615"},
216 UInt256{"DF4220E93ADC6F5569063A01B4DC79F8DB9553B6A3222ADE23DEA02BBE7230E5"},
217 };
218
219 SHAMap map{SHAMapType::FREE, f};
220 if (!testMode.backed)
221 map.setUnbacked();
222
223 EXPECT_EQ(map.getHash(), beast::kZero);
224 for (std::size_t k = 0; k < kKeys.size(); ++k)
225 {
226 EXPECT_TRUE(map.addItem(
228 makeShamapitem(kKeys[k], intToVuc(static_cast<std::uint8_t>(k)))));
229 EXPECT_EQ(map.getHash().asUInt256(), kHashes[k]);
230 map.invariants();
231 }
232 for (std::size_t k = kKeys.size(); k-- > 0;)
233 {
234 EXPECT_EQ(map.getHash().asUInt256(), kHashes[k]);
235 EXPECT_TRUE(map.delItem(kKeys[k]));
236 map.invariants();
237 }
238 EXPECT_EQ(map.getHash(), beast::kZero);
239 }
240
241 {
242 constexpr std::array kKeys{
243 UInt256{"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
244 UInt256{"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
245 UInt256{"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
246 UInt256{"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
247 UInt256{"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
248 UInt256{"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
249 UInt256{"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
250 UInt256{"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
251 };
252
254 SHAMap map{SHAMapType::FREE, tf};
255 if (!testMode.backed)
256 map.setUnbacked();
257 for (auto const& k : kKeys)
258 {
260 map.invariants();
261 }
262
263 auto keyIndex = kKeys.size();
264 for (auto const& k : map)
265 EXPECT_EQ(k.key(), kKeys[--keyIndex]);
266 }
267}
268
270 BackingMode,
272 ::testing::Values(kBackedMode, kUnbackedMode),
274
275// Exercises the traversal stacks built by belowHelper. Each stack entry pairs a node with the ID
276// naming its position, and SHAMap asserts that pairing on every push, so these traversals fail
277// loudly in a Debug build if a node ID is ever derived from the wrong branch.
278class SHAMapTraversal : public ::testing::Test
279{
280protected:
282
283 // Keys that share a long prefix and then fan out across distinct branches, so the deeper inner
284 // nodes have several children and traversal must descend many levels.
287 {
289 for (unsigned int branch = 0; branch < SHAMap::kBranchFactor; ++branch)
290 {
291 // Vary the 6th nibble, keeping the first five identical.
292 auto text = std::string("abcde") + "0123456789abcdef"[branch];
293 text.append(64 - text.size(), '7');
294 keys.emplace_back(std::string_view{text});
295 }
296 return keys;
297 }
298
299 // Keys that share all 63 leading nibbles and fan out only at the last one, so the tree is a
300 // chain of single-child inner nodes down to depth 63 with the leaves as siblings at depth 64.
301 // This exercises kLeafDepth directly, unlike deepFanOutKeys() above, whose fan-out at the 6th
302 // nibble keeps the tree only about 6 levels deep.
305 {
307 for (unsigned int branch = 0; branch < SHAMap::kBranchFactor; ++branch)
308 {
309 auto text = std::string(63, 'a') + "0123456789abcdef"[branch];
310 keys.emplace_back(std::string_view{text});
311 }
312 return keys;
313 }
314
315 static void
317 {
318 map.setUnbacked();
319 for (auto const& k : keys)
320 {
321 Buffer vuc{32};
322 std::fill_n(vuc.data(), vuc.size(), std::uint8_t{1});
323 EXPECT_TRUE(
324 map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, std::move(vuc))));
325 map.invariants();
326 }
327 }
328};
329
330TEST_F(SHAMapTraversal, forward_iteration_visits_every_key_in_order)
331{
333 auto keys = deepFanOutKeys();
334 SHAMap map{SHAMapType::FREE, f};
335 fillMap(map, keys);
336
337 std::ranges::sort(keys);
338 std::vector<UInt256> visited;
339 for (auto const& item : map)
340 visited.push_back(item.key());
341
342 EXPECT_EQ(visited, keys);
343}
344
345TEST_F(SHAMapTraversal, upper_bound_walks_the_whole_map)
346{
348 auto keys = deepFanOutKeys();
349 SHAMap map{SHAMapType::FREE, f};
350 fillMap(map, keys);
351 std::ranges::sort(keys);
352
353 // upperBound from each key must land on its successor, driving belowHelper across every
354 // subtree.
355 for (std::size_t k = 0; k + 1 < keys.size(); ++k)
356 {
357 auto it = map.upperBound(keys[k]);
358 ASSERT_NE(it, map.end()) << "no successor for key " << k;
359 EXPECT_EQ(it->key(), keys[k + 1]) << "wrong successor for key " << k;
360 }
361 EXPECT_EQ(map.upperBound(keys.back()), map.end());
362}
363
364TEST_F(SHAMapTraversal, lower_bound_walks_the_whole_map)
365{
367 auto keys = deepFanOutKeys();
368 SHAMap map{SHAMapType::FREE, f};
369 fillMap(map, keys);
370 std::ranges::sort(keys);
371
372 // lowerBound is the reverse direction: belowHelper descends to the greatest key below a
373 // subtree.
374 for (std::size_t k = 1; k < keys.size(); ++k)
375 {
376 auto it = map.lowerBound(keys[k]);
377 ASSERT_NE(it, map.end()) << "no predecessor for key " << k;
378 EXPECT_EQ(it->key(), keys[k - 1]) << "wrong predecessor for key " << k;
379 }
380 EXPECT_EQ(map.lowerBound(keys.front()), map.end());
381}
382
383TEST_F(SHAMapTraversal, bounds_agree_with_iteration_for_absent_keys)
384{
386 auto keys = deepFanOutKeys();
387 SHAMap map{SHAMapType::FREE, f};
388 fillMap(map, keys);
389 std::ranges::sort(keys);
390
391 // Probe keys that are not in the map, so the traversal starts mid-tree rather than at a leaf.
392 for (unsigned char const c : {0x00, 0x40, 0x80, 0xc0, 0xff})
393 {
394 UInt256 probe;
395 std::fill_n(probe.begin(), probe.size(), c);
396
397 auto const expectedUpper = std::ranges::upper_bound(keys, probe);
398 auto const upper = map.upperBound(probe);
399 if (expectedUpper == keys.end())
400 {
401 EXPECT_EQ(upper, map.end()) << "probe " << static_cast<unsigned>(c);
402 }
403 else
404 {
405 ASSERT_NE(upper, map.end()) << "probe " << static_cast<unsigned>(c);
406 EXPECT_EQ(upper->key(), *expectedUpper) << "probe " << static_cast<unsigned>(c);
407 }
408
409 auto const lowerCount = std::ranges::lower_bound(keys, probe) - keys.begin();
410 auto const lower = map.lowerBound(probe);
411 if (lowerCount == 0)
412 {
413 EXPECT_EQ(lower, map.end()) << "probe " << static_cast<unsigned>(c);
414 }
415 else
416 {
417 ASSERT_NE(lower, map.end()) << "probe " << static_cast<unsigned>(c);
418 EXPECT_EQ(lower->key(), keys[lowerCount - 1]) << "probe " << static_cast<unsigned>(c);
419 }
420 }
421
422 // Probe keys that land on a leaf and require the leaf-pop-and-resume case. Keys share
423 // "abcde" as a prefix and vary the 6th nibble, so a probe that shares the full prefix
424 // but differs in the padding hits a leaf from either side: '0' padded with '0' lands below
425 // the first key, and 'f' padded with 'f' lands above the last.
426 for (char const nibble : {'0', 'f'})
427 {
428 auto text = std::string("abcde") + nibble;
429 text.append(64 - text.size(), nibble);
430 UInt256 const probe{std::string_view{text}};
431
432 auto const expectedUpper = std::ranges::upper_bound(keys, probe);
433 auto const upper = map.upperBound(probe);
434 if (expectedUpper == keys.end())
435 {
436 EXPECT_EQ(upper, map.end()) << "nibble " << nibble;
437 }
438 else
439 {
440 ASSERT_NE(upper, map.end()) << "nibble " << nibble;
441 EXPECT_EQ(upper->key(), *expectedUpper) << "nibble " << nibble;
442 }
443
444 auto const lowerCount = std::ranges::lower_bound(keys, probe) - keys.begin();
445 auto const lower = map.lowerBound(probe);
446 if (lowerCount == 0)
447 {
448 EXPECT_EQ(lower, map.end()) << "nibble " << nibble;
449 }
450 else
451 {
452 ASSERT_NE(lower, map.end()) << "nibble " << nibble;
453 EXPECT_EQ(lower->key(), keys[lowerCount - 1]) << "nibble " << nibble;
454 }
455 }
456}
457
458TEST_F(SHAMapTraversal, bounds_on_empty_map_return_end)
459{
461 SHAMap map{SHAMapType::FREE, f};
462 map.setUnbacked();
463
464 // The root is a childless inner node, so boundHelper's inner-node branch scans every branch on
465 // the requested side of the one id selects, finds them all empty, and falls through to end()
466 // rather than dereference a child.
467 EXPECT_EQ(map.upperBound(UInt256{}), map.end());
468 EXPECT_EQ(map.lowerBound(UInt256{}), map.end());
469
470 UInt256 probe;
471 std::fill_n(probe.begin(), probe.size(), std::uint8_t{0xff});
472 EXPECT_EQ(map.upperBound(probe), map.end());
473 EXPECT_EQ(map.lowerBound(probe), map.end());
474}
475
476TEST_F(SHAMapTraversal, bounds_on_single_item_map_use_the_leaf_below_the_root)
477{
479 SHAMap map{SHAMapType::FREE, f};
480
481 auto const key = deepFanOutKeys().front();
482 fillMap(map, {key});
483
484 // fillMap adds items in-process, so root_ stays an inner node with the single leaf below it.
485 // The stack holds both, so boundHelper examines the leaf first.
486 UInt256 below = key;
487 --below;
488 UInt256 above = key;
489 ++above;
490
491 auto const upper = map.upperBound(below);
492 ASSERT_NE(upper, map.end());
493 EXPECT_EQ(upper->key(), key);
494 EXPECT_EQ(map.upperBound(key), map.end());
495 EXPECT_EQ(map.upperBound(above), map.end());
496
497 auto const lower = map.lowerBound(above);
498 ASSERT_NE(lower, map.end());
499 EXPECT_EQ(lower->key(), key);
500 EXPECT_EQ(map.lowerBound(key), map.end());
501 EXPECT_EQ(map.lowerBound(below), map.end());
502}
503
504TEST_F(SHAMapTraversal, iteration_survives_deletions)
505{
507 auto keys = deepFanOutKeys();
508 SHAMap map{SHAMapType::FREE, f};
509 fillMap(map, keys);
510 std::ranges::sort(keys);
511
512 // Deleting every other key drops the fan-out node's branch count from 16 to 8, never the 1
513 // that would make delItem collapse it into a leaf. So this pins that iteration survives
514 // deletions that reshape the map without collapsing any inner node; the case that does
515 // collapse one is iteration_survives_a_collapsed_inner_node below.
516 for (std::size_t k = 0; k < keys.size(); k += 2)
517 {
518 ASSERT_TRUE(map.delItem(keys[k]));
519 map.invariants();
520 }
521
522 std::vector<UInt256> expected;
523 for (std::size_t k = 1; k < keys.size(); k += 2)
524 expected.push_back(keys[k]);
525
526 std::vector<UInt256> visited;
527 for (auto const& item : map)
528 visited.push_back(item.key());
529 EXPECT_EQ(visited, expected);
530
531 for (std::size_t k = 0; k + 1 < expected.size(); ++k)
532 {
533 auto it = map.upperBound(expected[k]);
534 ASSERT_NE(it, map.end());
535 EXPECT_EQ(it->key(), expected[k + 1]);
536 }
537}
538
539TEST_F(SHAMapTraversal, iteration_survives_a_collapsed_inner_node)
540{
542 SHAMap map{SHAMapType::FREE, f};
543
544 // One key in a separate subtree, diverging from the fan-out group at the very first nibble, so
545 // it survives untouched while the fan-out group below is collapsed.
546 auto const sentinel = UInt256{std::string_view{std::string(64, '0')}};
547
548 auto fanOutKeys = deepFanOutKeysAtLeafDepth();
549 fillMap(map, fanOutKeys);
550 Buffer vuc{32};
551 std::fill_n(vuc.data(), vuc.size(), std::uint8_t{1});
552 ASSERT_TRUE(
553 map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(sentinel, std::move(vuc))));
554 map.invariants();
555
556 std::ranges::sort(fanOutKeys);
557
558 // Delete all but the last fan-out key. The fan-out node's branch count drops to 1 on the final
559 // delete, which delItem collapses by pulling the sole remaining leaf up in its place; every
560 // ancestor above it has exactly one child by construction, so each of those also drops to
561 // branch count 1 and collapses in turn, all the way up to (but not including) the root. That
562 // final delete replaces the entire 63-level chain with the root pointing straight at the one
563 // remaining leaf, so the surviving traversal stack is rebuilt over a drastically different tree
564 // shape, not just missing one inner node.
565 for (std::size_t k = 0; k + 1 < fanOutKeys.size(); ++k)
566 {
567 ASSERT_TRUE(map.delItem(fanOutKeys[k]));
568 map.invariants();
569 }
570
571 std::vector<UInt256> const expected{sentinel, fanOutKeys.back()};
572 std::vector<UInt256> visited;
573 for (auto const& item : map)
574 visited.push_back(item.key());
575 EXPECT_EQ(visited, expected);
576
577 auto it = map.upperBound(sentinel);
578 ASSERT_NE(it, map.end());
579 EXPECT_EQ(it->key(), fanOutKeys.back());
580 EXPECT_EQ(map.upperBound(fanOutKeys.back()), map.end());
581}
582
583// The tests below mirror the ones above but use deepFanOutKeysAtLeafDepth(), whose keys share all
584// 63 leading nibbles and fan out only at the last one. That puts the leaves at depth
585// SHAMap::kLeafDepth, so these traversals walk a chain of single-child inner nodes all the way down
586// and exercise the kLeafDepth guards that deepFanOutKeys() alone (fanning out at the 6th nibble)
587// never reaches.
588
589TEST_F(SHAMapTraversal, forward_iteration_visits_every_key_in_order_at_leaf_depth)
590{
592 auto keys = deepFanOutKeysAtLeafDepth();
593 SHAMap map{SHAMapType::FREE, f};
594 fillMap(map, keys);
595
596 std::ranges::sort(keys);
597 std::vector<UInt256> visited;
598 for (auto const& item : map)
599 visited.push_back(item.key());
600
601 EXPECT_EQ(visited, keys);
602}
603
604TEST_F(SHAMapTraversal, upper_bound_walks_the_whole_map_at_leaf_depth)
605{
607 auto keys = deepFanOutKeysAtLeafDepth();
608 SHAMap map{SHAMapType::FREE, f};
609 fillMap(map, keys);
610 std::ranges::sort(keys);
611
612 // upperBound from each key must land on its successor, driving belowHelper down to depth
613 // kLeafDepth for every subtree.
614 for (std::size_t k = 0; k + 1 < keys.size(); ++k)
615 {
616 auto it = map.upperBound(keys[k]);
617 ASSERT_NE(it, map.end()) << "no successor for key " << k;
618 EXPECT_EQ(it->key(), keys[k + 1]) << "wrong successor for key " << k;
619 }
620 EXPECT_EQ(map.upperBound(keys.back()), map.end());
621}
622
623TEST_F(SHAMapTraversal, lower_bound_walks_the_whole_map_at_leaf_depth)
624{
626 auto keys = deepFanOutKeysAtLeafDepth();
627 SHAMap map{SHAMapType::FREE, f};
628 fillMap(map, keys);
629 std::ranges::sort(keys);
630
631 // lowerBound is the reverse direction: belowHelper descends to depth kLeafDepth to find the
632 // greatest key below a subtree.
633 for (std::size_t k = 1; k < keys.size(); ++k)
634 {
635 auto it = map.lowerBound(keys[k]);
636 ASSERT_NE(it, map.end()) << "no predecessor for key " << k;
637 EXPECT_EQ(it->key(), keys[k - 1]) << "wrong predecessor for key " << k;
638 }
639 EXPECT_EQ(map.lowerBound(keys.front()), map.end());
640}
641
642TEST_F(SHAMapTraversal, bounds_agree_with_iteration_for_absent_keys_at_leaf_depth)
643{
645 auto keys = deepFanOutKeysAtLeafDepth();
646 SHAMap map{SHAMapType::FREE, f};
647 fillMap(map, keys);
648 std::ranges::sort(keys);
649
650 // The keys fill all 16 branches of the last nibble, so an absent key must diverge from the
651 // shared 'a' prefix earlier than that. Diverging at increasingly deep nibbles forces
652 // walkTowardsKey to descend through more single-child inner nodes before it finds the empty
653 // branch, right up to the one just above kLeafDepth.
654 for (unsigned int const divergeAt : {0u, 31u, 61u, 62u})
655 {
656 // '9' sorts below the shared 'a' prefix and 'b' above it, so the probe lands under or
657 // over the whole key block -- driving belowHelper's First and Last descents respectively.
658 for (char const nibble : {'9', 'b'})
659 {
660 auto text = std::string(divergeAt, 'a') + nibble;
661 text.append(64 - text.size(), '0');
662 UInt256 const probe{std::string_view{text}};
663
664 auto const expectedUpper = std::ranges::upper_bound(keys, probe);
665 auto const upper = map.upperBound(probe);
666 if (expectedUpper == keys.end())
667 {
668 EXPECT_EQ(upper, map.end()) << "divergeAt " << divergeAt << " nibble " << nibble;
669 }
670 else
671 {
672 ASSERT_NE(upper, map.end()) << "divergeAt " << divergeAt << " nibble " << nibble;
673 EXPECT_EQ(upper->key(), *expectedUpper)
674 << "divergeAt " << divergeAt << " nibble " << nibble;
675 }
676
677 auto const lowerCount = std::ranges::lower_bound(keys, probe) - keys.begin();
678 auto const lower = map.lowerBound(probe);
679 if (lowerCount == 0)
680 {
681 EXPECT_EQ(lower, map.end()) << "divergeAt " << divergeAt << " nibble " << nibble;
682 }
683 else
684 {
685 ASSERT_NE(lower, map.end()) << "divergeAt " << divergeAt << " nibble " << nibble;
686 EXPECT_EQ(lower->key(), keys[lowerCount - 1])
687 << "divergeAt " << divergeAt << " nibble " << nibble;
688 }
689 }
690 }
691}
692
693TEST_F(SHAMapTraversal, iteration_survives_deletions_at_leaf_depth)
694{
696 auto keys = deepFanOutKeysAtLeafDepth();
697 SHAMap map{SHAMapType::FREE, f};
698 fillMap(map, keys);
699 std::ranges::sort(keys);
700
701 // Deleting every other key drops the fan-out node's branch count from 16 to 8, the same
702 // non-collapsing case as iteration_survives_deletions above, but reached by descending through
703 // a chain of single-child inner nodes down to kLeafDepth instead of a shallow one.
704 for (std::size_t k = 0; k < keys.size(); k += 2)
705 {
706 ASSERT_TRUE(map.delItem(keys[k]));
707 map.invariants();
708 }
709
710 std::vector<UInt256> expected;
711 for (std::size_t k = 1; k < keys.size(); k += 2)
712 expected.push_back(keys[k]);
713
714 std::vector<UInt256> visited;
715 for (auto const& item : map)
716 visited.push_back(item.key());
717 EXPECT_EQ(visited, expected);
718
719 for (std::size_t k = 0; k + 1 < expected.size(); ++k)
720 {
721 auto it = map.upperBound(expected[k]);
722 ASSERT_NE(it, map.end());
723 EXPECT_EQ(it->key(), expected[k + 1]);
724 }
725}
726
727class SHAMapPathProof : public ::testing::Test
728{
729protected:
731};
732
733TEST_F(SHAMapPathProof, verify_proof_path)
734{
736 SHAMap map{SHAMapType::FREE, tf};
737 map.setUnbacked();
738
739 UInt256 key;
740 UInt256 rootHash;
741 std::vector<Blob> goodPath;
742
743 static constexpr unsigned char kFirstKey = 1;
744 static constexpr unsigned char kKeyCount = 100;
745 static constexpr unsigned char kLastKey = kKeyCount - 1;
746
747 for (unsigned char c = kFirstKey; c < kKeyCount; ++c)
748 {
749 UInt256 k(c);
751 map.invariants();
752
753 auto root = map.getHash().asUInt256();
754 auto path = map.getProofPath(k);
755 if (!path)
756 {
757 ADD_FAILURE() << "Missing proof path";
758 return;
759 }
760 auto& proofPath = *path;
761
762 EXPECT_TRUE(map.verifyProofPath(root, k, proofPath));
763 if (c == kFirstKey)
764 {
765 // extra node
766 proofPath.insert(proofPath.begin(), proofPath.front());
767 EXPECT_FALSE(map.verifyProofPath(root, k, proofPath));
768 // wrong key
769 UInt256 const wrongKey(c + 1);
770 EXPECT_FALSE(map.getProofPath(wrongKey));
771 }
772 if (c == kLastKey)
773 {
774 key = k;
775 rootHash = root;
776 goodPath = std::move(proofPath);
777 }
778 }
779
780 // still good
781 EXPECT_TRUE(map.verifyProofPath(rootHash, key, goodPath));
782 // empty path
783 std::vector<Blob> badPath;
784 EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
785 // too long
786 badPath = goodPath;
787 badPath.push_back(goodPath.back());
788 EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
789 // bad node
790 badPath.clear();
791 badPath.emplace_back(100, 100);
792 EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
793 // bad node type
794 badPath.clear();
795 badPath.push_back(goodPath.front());
796 badPath.front().back()--; // change node type
797 EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
798 // all inner
799 badPath.clear();
800 badPath = goodPath;
801 badPath.erase(badPath.begin());
802 EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
803}
804
805// A legitimate proof path for two keys sharing all 63 leading nibbles is 65 elements: inner nodes
806// at depths 0..63 plus the leaf at depth 64. This pins that the 65 bound is real, so the fix for
807// the forged-path case below must not simply tighten the length limit.
808TEST_F(SHAMapPathProof, legitimate_deep_path_is_sixty_five_elements)
809{
811 SHAMap map{SHAMapType::FREE, f};
812 map.setUnbacked();
813
814 auto const kA = UInt256{std::string_view{std::string(63, 'a') + "1"}};
815 auto const kB = UInt256{std::string_view{std::string(63, 'a') + "2"}};
816
817 for (auto const& k : {kA, kB})
818 {
819 Buffer vuc{32};
820 std::fill_n(vuc.data(), vuc.size(), std::uint8_t{1});
821 ASSERT_TRUE(map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, std::move(vuc))));
822 }
823 map.invariants();
824
825 auto const pathA = map.getProofPath(kA);
826 ASSERT_TRUE(pathA.has_value());
827 // NOLINTBEGIN(bugprone-unchecked-optional-access) has_value() checked above
828 EXPECT_EQ(pathA->size(), 65u);
829 EXPECT_TRUE(SHAMap::verifyProofPath(map.getHash().asUInt256(), kA, *pathA));
830 // NOLINTEND(bugprone-unchecked-optional-access)
831
832 auto const pathB = map.getProofPath(kB);
833 ASSERT_TRUE(pathB.has_value());
834 // NOLINTBEGIN(bugprone-unchecked-optional-access) has_value() checked above
835 EXPECT_EQ(pathB->size(), 65u);
836 EXPECT_TRUE(SHAMap::verifyProofPath(map.getHash().asUInt256(), kB, *pathB));
837 // NOLINTEND(bugprone-unchecked-optional-access)
838}
839
840// A forged path of 65 hash-chained inner nodes reaches depth kLeafDepth, where only the leaf
841// terminating the path may sit. Such a path must be rejected.
842TEST_F(SHAMapPathProof, all_inner_path_at_leaf_depth_is_rejected)
843{
844 // An arbitrary well-formed key; the test does not care about its specific value.
845 constexpr UInt256 kTestKey("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
846
847 // Build upwards from the deepest node so each parent's selected branch carries its child's hash
848 // and the hash chain validates at every level.
850 SHAMapHash childHash{UInt256{1}};
851
852 for (auto depth = SHAMap::kLeafDepth + 1u; depth-- > 0;)
853 {
854 auto const id = SHAMapNodeID::createID(std::min(depth, SHAMap::kLeafDepth - 1u), kTestKey);
855 auto const branch = selectBranch(id, kTestKey);
856
857 Serializer s;
858 for (auto i = 0u; i < SHAMap::kBranchFactor; ++i)
859 s.addBitString(i == branch ? childHash.asUInt256() : UInt256{});
861 path.push_back(s.getData());
862
863 auto node = SHAMapTreeNode::makeFromWire(makeSlice(path.back()));
864 ASSERT_TRUE(node);
865 node->updateHash();
866 childHash = node->getHash();
867 }
868
869 ASSERT_EQ(path.size(), 65u);
870 EXPECT_FALSE(SHAMap::verifyProofPath(childHash.asUInt256(), kTestKey, path));
871}
872
885forgeRootOverLeaf(Blob const& leafBlob, UInt256 const& key)
886{
887 auto leaf = SHAMapTreeNode::makeFromWire(makeSlice(leafBlob));
888 if (!leaf || !leaf->isLeaf())
889 return {};
890 leaf->updateHash();
891
892 auto const branch = selectBranch(SHAMapNodeID::createID(0, key), key);
893 Serializer s;
894 for (auto i = 0u; i < SHAMap::kBranchFactor; ++i)
895 s.addBitString(i == branch ? leaf->getHash().asUInt256() : UInt256{});
897
899 if (!root)
900 return {};
901 root->updateHash();
902
903 return {std::vector<Blob>{leafBlob, s.getData()}, root->getHash().asUInt256()};
904}
905
906// The hash chain above a leaf proves nothing about which key that leaf holds, so a peer can graft a
907// genuine leaf from elsewhere in the map onto a path forged for another key. Comparing the terminal
908// leaf's own key against the key being proved is what rejects it.
909TEST_F(SHAMapPathProof, substituted_leaf_for_other_key_is_rejected)
910{
912 SHAMap map{SHAMapType::FREE, f};
913 map.setUnbacked();
914
915 // Two arbitrary keys differing in their first nibble, so each leaf hangs off the root directly.
916 constexpr UInt256 kKey("1c8cec8e5e9b0e5e0e0f5b3e2c9f7a1d6b4e8c2a0d7f3b9e5c1a8d4f2b6e0c93");
917 constexpr UInt256 kOtherKey("e3f1a7d5b9c2e8f406a1d3b5c7e9f2a4d6b8c0e2f4a6d8b0c2e4f6a8d0b2c4e6");
918
919 for (auto const& k : {kKey, kOtherKey})
920 {
921 ASSERT_TRUE(map.addItem(
922 SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()})));
923 }
924 map.invariants();
925
926 auto const ownPath = map.getProofPath(kKey);
927 auto const otherPath = map.getProofPath(kOtherKey);
928 ASSERT_TRUE(ownPath.has_value());
929 ASSERT_TRUE(otherPath.has_value());
930
931 // NOLINTBEGIN(bugprone-unchecked-optional-access) has_value() checked above
932 // The genuine leaf blobs, deepest element first.
933 auto const& ownLeaf = ownPath->front();
934 auto const& otherLeaf = otherPath->front();
935 // NOLINTEND(bugprone-unchecked-optional-access)
936
937 // Control: the forged root is accepted when the leaf below it really is kKey's leaf, so the
938 // rejection below can only come from the leaf key comparison.
939 auto const [goodPath, goodRoot] = forgeRootOverLeaf(ownLeaf, kKey);
940 ASSERT_EQ(goodPath.size(), 2u);
941 EXPECT_TRUE(SHAMap::verifyProofPath(goodRoot, kKey, goodPath));
942
943 // Same forged root, but kOtherKey's leaf substituted at the bottom: the hash chain still
944 // validates, yet the path does not prove anything about kKey.
945 auto const [badPath, badRoot] = forgeRootOverLeaf(otherLeaf, kKey);
946 ASSERT_EQ(badPath.size(), 2u);
947 EXPECT_FALSE(SHAMap::verifyProofPath(badRoot, kKey, badPath));
948}
949
950} // namespace xrpl::tests
T append(T... args)
T back(T... args)
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
pointer data()
Definition base_uint.h:117
iterator begin()
Definition base_uint.h:128
static constexpr std::size_t size()
Definition base_uint.h:548
Like std::vector<char> but better.
Definition Buffer.h:19
std::size_t size() const noexcept
Returns the number of bytes in the buffer.
Definition Buffer.h:123
std::uint8_t const * data() const noexcept
Return a pointer to beginning of the storage.
Definition Buffer.h:148
void fill(std::uint8_t value) noexcept
Set every byte in the buffer to the given value.
Definition Buffer.h:168
UInt256 const & asUInt256() const
Definition SHAMapHash.h:26
UInt256 const & key() const
Definition SHAMapItem.h:74
static SHAMapNodeID createID(unsigned int depth, UInt256 const &key)
Create a SHAMapNodeID of a node with the depth of the node and the key of a leaf.
static SHAMapTreeNodePtr makeFromWire(Slice rawNode)
bool addItem(SHAMapNodeType type, boost::intrusive_ptr< SHAMapItem const > item)
ConstIterator upperBound(UInt256 const &id) const
Find the first item after the given item.
static constexpr unsigned int kLeafDepth
The depth of the hash map: data is only present in the leaves.
Definition SHAMap.h:144
static constexpr unsigned int kBranchFactor
Number of children each non-leaf node has (the 'radix tree' part of the map).
Definition SHAMap.h:139
void setUnbacked()
Definition SHAMap.h:785
void dump(bool withHashes=false) const
std::map< UInt256, DeltaItem > Delta
Definition SHAMap.h:148
bool compare(SHAMap const &otherMap, Delta &differences, int maxCount) const
std::shared_ptr< SHAMap > snapShot(bool isMutable) const
bool delItem(UInt256 const &id)
ConstIterator end() const
Definition SHAMap.h:903
ConstIterator lowerBound(UInt256 const &id) const
Find the object with the greatest object id smaller than the input id.
SHAMapHash getHash() const
ConstIterator begin() const
Definition SHAMap.h:897
std::optional< std::vector< Blob > > getProofPath(UInt256 const &key) const
Get the proof path of the key.
static bool verifyProofPath(UInt256 const &rootHash, UInt256 const &key, std::vector< Blob > const &path)
Verify the proof path.
Blob const & peekData() const
Definition Serializer.h:273
int addBitString(BaseUInt< Bits, Tag > const &v)
Definition Serializer.h:202
int add8(unsigned char byteValue)
Blob getData() const
Definition Serializer.h:278
An immutable linear range of bytes.
Definition Slice.h:28
static TestSink & instance()
Definition TestSink.h:12
static Buffer intToVuc(std::uint8_t v)
static std::vector< UInt256 > deepFanOutKeysAtLeafDepth()
static std::vector< UInt256 > deepFanOutKeys()
static void fillMap(SHAMap &map, std::vector< UInt256 > const &keys)
T clear(T... args)
T emplace_back(T... args)
T empty(T... args)
T erase(T... args)
T fill_n(T... args)
T front(T... args)
T lower_bound(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
TEST_F(SHAMapTraversal, forward_iteration_visits_every_key_in_order)
constexpr SHAMapBackingMode kUnbackedMode
constexpr SHAMapBackingMode kBackedMode
TEST_P(SHAMapTest, add_traverse_snapshot_build_tear_and_iterate)
constexpr UInt256 kTestKey("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8")
bool operator!=(SHAMapItem const &a, SHAMapItem const &b)
static std::pair< std::vector< Blob >, UInt256 > forgeRootOverLeaf(Blob const &leafBlob, UInt256 const &key)
Wrap a leaf blob in a forged root inner node whose branch for key carries that leaf's hash.
std::string shamapBackingModeName(::testing::TestParamInfo< SHAMapBackingMode > const &info)
INSTANTIATE_TEST_SUITE_P(BackingMode, SHAMapTest, ::testing::Values(kBackedMode, kUnbackedMode), shamapBackingModeName)
Number root(Number f, unsigned d)
Slice makeSlice(std::array< T, N > const &a)
Definition Slice.h:228
BaseUInt< 256 > UInt256
Definition base_uint.h:580
boost::intrusive_ptr< SHAMapItem > makeShamapitem(UInt256 const &tag, Slice data)
Definition SHAMapItem.h:148
unsigned int selectBranch(SHAMapNodeID const &id, UInt256 const &hash)
Returns the branch that would contain the given hash.
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:11
static constexpr unsigned char const kWireTypeInner
T push_back(T... args)
T size(T... args)
T sort(T... args)
T upper_bound(T... args)