Compare commits

...

115 Commits

Author SHA1 Message Date
Bart
2b2c02a443 Merge branch 'develop' into bthomee/node_depth 2026-07-06 15:04:35 -07:00
Bart
289d17aa10 Clang-tidy changes 2026-07-06 15:04:10 -07:00
Bart
4afd978be3 Merge branch 'develop' into bthomee/node_depth 2026-07-06 13:23:04 -07:00
Marek Foss
f5e63f8a91 test: Migrate basics Beast tests to GTest (#7136)
Co-authored-by: Alex Kremer <akremer@ripple.com>
2026-07-06 19:48:36 +00:00
Marek Foss
7a153a2bce test: Migrate resource, shamap Beast tests to GTest (#7133)
Co-authored-by: Alex Kremer <akremer@ripple.com>
2026-07-06 17:26:19 +00:00
Ayaz Salikhov
f59d5c0894 chore: Enable most misc checks (#7663) 2026-07-06 15:22:11 +00:00
Bart
81d5f3f4ae Clang-tidy changes 2026-07-02 11:28:41 -04:00
Bart
5883e66120 Merge branch 'develop' into bthomee/node_depth 2026-07-02 11:15:40 -04:00
Bart
b8f8afb82f Clang-tidy changes 2026-07-02 10:14:03 -04:00
Bart
89fff13b35 Merge branch 'develop' into bthomee/node_depth 2026-07-02 06:02:28 -04:00
Bart
6da2b9e925 Restore serializer, add node check 2026-06-23 13:24:31 -04:00
Bart
b156c19f82 Merge branch 'develop' into bthomee/node_depth 2026-06-23 10:54:00 -04:00
Bart
eda7ec104a Merge branch 'develop' into bthomee/node_depth 2026-06-05 17:48:43 -04:00
Bart
6ffef55fac Optimize handling, support relaying to old node 2026-06-05 17:48:16 -04:00
Bart
b82baaec0f Merge branch 'develop' into bthomee/node_depth 2026-06-05 10:51:08 -04:00
Bart
5f59b7f4ad Merge branch 'develop' into bthomee/node_depth 2026-06-03 12:34:42 -04:00
Bart
b8be772441 Charge peers for invalid data 2026-06-02 14:21:21 -04:00
Bart
0c7873c9f1 Merge branch 'develop' into bthomee/node_depth 2026-06-02 14:06:01 -04:00
Bart
ec290777c5 Merge branch 'develop' into bthomee/node_depth 2026-05-31 17:54:25 -04:00
Bart
cd7babf4b6 Merge branch 'develop' into bthomee/node_depth 2026-05-28 11:54:05 -04:00
Bart
3f4d730a61 Merge branch 'develop' into bthomee/node_depth 2026-05-21 12:20:19 +01:00
Bart
41b258ae73 Merge branch 'develop' into bthomee/node_depth 2026-05-15 13:17:41 -04:00
Bart
cf8744ebe3 Merge branch 'develop' into bthomee/node_depth 2026-05-14 15:09:05 -04:00
Bart
6f7c6a79f5 Try removing SHAMapNodeData constructor 2026-05-12 08:30:50 -04:00
Bart
35ffa71c0b Review feedback 2026-05-11 17:53:43 -04:00
Bart
4e8b6c4176 Merge branch 'develop' into bthomee/node_depth 2026-05-11 17:48:34 -04:00
Bart
081d86653f Update levelization 2026-05-11 09:57:25 -04:00
Bart
4ba60eae21 Review feedback 2026-05-11 09:45:01 -04:00
Bart
e21f879437 Merge branch 'develop' into bthomee/node_depth 2026-05-11 09:30:39 -04:00
Bart
5023558e11 Merge branch 'develop' into bthomee/node_depth 2026-05-07 16:07:16 -04:00
Bart
4b83169922 Address AI feedback 2026-05-03 19:51:58 -04:00
Bart
d5d79c1d09 Apply clang-tidy diff 2026-05-03 18:16:19 -04:00
Bart
7912d112e7 Address AI feedback 2026-05-03 17:48:25 -04:00
Bart
1a35abbb43 Apply clang-tidy fixes 2026-05-03 09:24:59 -04:00
Bart
95e3267b07 Merge branch 'develop' into bthomee/node_depth 2026-05-03 08:55:50 -04:00
Bart
42e5683385 Merge branch 'develop' into bthomee/node_depth 2026-04-29 13:57:28 -04:00
Bart
fe6ddccabb Merge branch 'develop' into bthomee/node_depth 2026-04-28 13:43:22 -04:00
Bart
49faf696d4 Address Copilot feedback 2026-04-26 11:48:58 -04:00
Bart
5b5ad09c53 Merge branch 'develop' into bthomee/node_depth 2026-04-24 14:44:42 -04:00
Bart
2405a3353c Add [[nodiscard]] to getNodeFat 2026-04-24 09:52:39 -04:00
Bart
705622011b Merge branch 'develop' into bthomee/node_depth 2026-04-24 09:47:14 -04:00
Bart
ae5f5cb92b Use emplace_back instead of push_back 2026-04-24 09:45:03 -04:00
Bart
cde8f17b5d Merge branch 'develop' into bthomee/node_depth 2026-04-24 09:26:37 -04:00
Bart
c8fb69ee1a Address Copilot feedback 2026-04-23 19:02:21 -07:00
Bart
430f770f2a Apply clang-tidy diff 2026-04-23 18:52:19 -07:00
Bart
44590a7008 Review feedback 2026-04-23 18:15:01 -07:00
Bart
1934c316b2 Update levelization 2026-04-23 15:46:24 -07:00
Bart
fd2a8b5825 Apply clang-tidy diff 2026-04-23 15:43:59 -07:00
Bart
012144c16c Merge branch 'develop' into bthomee/node_depth 2026-04-23 15:30:19 -07:00
Bart
8e2d949680 Review feedback 2026-04-23 15:01:01 -07:00
Bart
b781018fee Merge branch 'develop' into bthomee/node_depth 2026-04-23 04:50:55 -07:00
Bart
d867c9b26d Address Copilot feedback 2026-04-21 15:29:50 -07:00
Bart
7749ed8488 Address Copilot feedback 2026-04-21 15:03:32 -07:00
Bart
4d01cac564 Update levelization 2026-04-21 15:00:07 -07:00
Bart
98e1ad2dec Add more clang-tidy fixes 2026-04-21 14:42:21 -07:00
Bart
342171bf20 Merge branch 'develop' into bthomee/node_depth 2026-04-21 14:06:15 -07:00
Bart
fe74f48e7a Add more clang-tidy fixes 2026-04-21 13:29:19 -07:00
Bart
d6e05cf513 Add more clang-tidy fixes 2026-04-21 11:28:40 -07:00
Bart
e31419aa06 Address Copilot feedback 2026-04-21 11:08:02 -07:00
Bart
05b4c67b96 Restore invalid clang-tidy fix 2026-04-21 10:58:12 -07:00
Bart
2524476124 Update levelization 2026-04-21 10:26:25 -07:00
Bart
8007788d77 Merge branch 'develop' into bthomee/node_depth 2026-04-21 10:25:58 -07:00
Bart
809395a59c Merge branch 'develop' into bthomee/node_depth 2026-04-20 11:21:40 -04:00
Bart
3ef64e019b Merge branch 'develop' into bthomee/node_depth 2026-04-02 16:56:09 -04:00
Bart
228ad1e98f Fix clang-tidy findings 2026-03-31 17:33:15 -04:00
Bart
c4b342a027 Merge branch 'develop' into bthomee/node_depth 2026-03-31 17:12:43 -04:00
Bart
556d80e724 Fix clang-tidy findings 2026-03-31 16:31:24 -04:00
Bart
044f1e67b9 Merge branch 'develop' into bthomee/node_depth 2026-03-31 14:00:34 -04:00
Bart
0ad72fae8f Merge branch 'develop' into bthomee/node_depth 2026-03-22 06:33:02 -04:00
Bart
cc383c5fb8 Clang-tidy fixes 2026-03-18 15:09:46 -04:00
Bart
88715f1e5a Merge branch 'develop' into bthomee/node_depth 2026-03-18 13:38:55 -04:00
Bart
e1477cef0c Fix merge conflict 2026-03-18 09:56:08 -04:00
Bart
942874d7b0 Merge branch 'develop' into bthomee/node_depth 2026-03-18 09:47:16 -04:00
Bart
79326fc6b5 Merge branch 'develop' into bthomee/node_depth 2026-03-16 19:48:17 -04:00
Bart
48535d5226 Merge branch 'develop' into bthomee/node_depth 2026-03-10 17:11:14 +01:00
Bart
d1a6558080 Merge branch 'develop' into bthomee/node_depth 2026-03-10 15:39:32 +01:00
Bart
84f86b354f Add defensive check 2026-03-06 13:28:39 +01:00
Bart
40a3985b02 Merge branch 'develop' into bthomee/node_depth 2026-03-06 09:27:59 +01:00
Bart
208bd35d45 Merge branch 'develop' into bthomee/node_depth 2026-03-05 08:36:13 +01:00
Bart
e90fbbf7b2 Copilot review feedback 2026-03-02 13:39:29 -05:00
Bart
277450e648 Merge branch 'develop' into bthomee/node_depth 2026-03-02 12:16:33 -05:00
Bart
e6993524ea Copilot review feedback 2026-03-02 12:10:12 -05:00
Bart
b117ecc6a2 Use std::string_view 2026-03-02 11:58:57 -05:00
Bart
6c3b00c342 Change takeNodes vector argument to r-value 2026-03-02 09:41:23 -05:00
Bart
8c296a935a Improve docstring 2026-03-01 17:44:48 -05:00
Bart
573ba82181 Copilot review feedback 2026-03-01 17:33:03 -05:00
Bart
1542ab7e27 Copilot review feedback 2026-03-01 17:04:04 -05:00
Bart
6374f4886d Support leaf nodes at depth 0, use std::move, simplify tests 2026-03-01 16:44:58 -05:00
Bart
ebf336f472 Copilot review feedback 2026-03-01 15:10:57 -05:00
Bart
ddc15ad612 Copilot review feedback 2026-03-01 14:48:27 -05:00
Bart
82db6ac498 Restore try-catch to protect against other exceptions 2026-03-01 13:33:39 -05:00
Bart
f749c41306 Add log message for consistency 2026-02-28 17:31:22 -05:00
Bart
f25e47a58d Improve comment 2026-02-28 17:27:15 -05:00
Bart
2396799bd8 Update levelization 2026-02-28 16:03:02 -05:00
Bart
4855b9f96a Improve function docstrings, fix tests 2026-02-28 15:46:49 -05:00
Bart
b2f65cb7eb Fix protocol version test 2026-02-28 14:40:55 -05:00
Bart
c523673885 Update levelization 2026-02-28 14:00:13 -05:00
Bart
caac4d63d3 Merge branch 'develop' into bthomee/node_depth 2026-02-28 13:56:37 -05:00
Bart
29b0076fa8 Use new protocol version instead of amendment, add tests 2026-02-28 13:54:00 -05:00
Bart
c9aa1094a7 Update docstrings 2026-02-27 12:55:14 -05:00
Bart
b86f69cb82 Merge branch 'develop' into bthomee/node_depth 2026-02-26 17:33:25 -05:00
Bart
5d0bf78512 Clang-format to 100 line length 2026-02-26 17:09:20 -05:00
Bart
554df631c6 Remove pragma once 2026-02-18 08:28:56 -05:00
Bart
5e704bfdfb Merge branch 'develop' into bthomee/node_depth 2026-02-18 08:02:52 -05:00
Bart
fe8cc02bfa Refine 2026-02-18 07:54:33 -05:00
Bart
061c033f52 Use oneof in proto message 2026-02-16 16:50:40 -05:00
Bart
832a7e7e4a Remove depth, do not include node ID for leaf nodes 2026-02-13 17:05:05 -05:00
Bart
b2371c4c02 Fixes 2026-02-13 15:47:08 -05:00
Bart
b94a7c4b44 Merge branch 'develop' into bthomee/node_depth 2026-02-13 11:46:56 -05:00
Bart
9b9027112d Use helper functions 2026-02-13 11:44:58 -05:00
Bart
8e7889c66e Refactor 2026-02-12 16:55:38 -05:00
Bart
d836c3788d Merge branch 'develop' into bthomee/node_depth 2026-02-12 15:33:13 -05:00
Bart
1cb7c0293f Check if amendment is enabled 2026-02-12 06:31:32 -05:00
Bart
52dabc1f79 Remove deprecated stanza on nodeid field 2026-02-11 16:28:35 -05:00
Bart
2d78d41f7b perf: Replace node ID by depth in TMLedgerNode 2026-02-11 15:55:16 -05:00
62 changed files with 5691 additions and 5346 deletions

View File

@@ -45,20 +45,10 @@ Checks: "-*,
llvm-namespace-comment,
misc-*,
-misc-anonymous-namespace-in-header,
-misc-confusable-identifiers,
-misc-coroutine-hostile-raii,
-misc-misleading-bidirectional,
-misc-misleading-identifier,
-misc-multiple-inheritance,
-misc-new-delete-overloads,
-misc-no-recursion,
-misc-non-copyable-objects,
-misc-non-private-member-variables-in-classes,
-misc-override-with-different-visibility,
-misc-predictable-rand,
-misc-unconventional-assign-operator,
-misc-uniqueptr-reset-release,
-misc-unused-parameters,
-misc-use-anonymous-namespace,
-misc-use-internal-linkage,

View File

@@ -72,7 +72,6 @@ test.app > xrpl.server
test.app > xrpl.shamap
test.app > xrpl.tx
test.basics > test.jtx
test.basics > test.unit_test
test.basics > xrpl.basics
test.basics > xrpl.core
test.basics > xrpld.rpc
@@ -162,9 +161,6 @@ test.protocol > test.unit_test
test.protocol > xrpl.basics
test.protocol > xrpl.json
test.protocol > xrpl.protocol
test.resource > test.unit_test
test.resource > xrpl.basics
test.resource > xrpl.resource
test.rpc > test.jtx
test.rpc > xrpl.basics
test.rpc > xrpl.config
@@ -188,12 +184,6 @@ test.server > xrpld.core
test.server > xrpl.json
test.server > xrpl.protocol
test.server > xrpl.server
test.shamap > test.unit_test
test.shamap > xrpl.basics
test.shamap > xrpl.config
test.shamap > xrpl.nodestore
test.shamap > xrpl.protocol
test.shamap > xrpl.shamap
test.unit_test > xrpl.basics
test.unit_test > xrpl.protocol
tests.libxrpl > xrpl.basics
@@ -205,6 +195,7 @@ tests.libxrpl > xrpl.net
tests.libxrpl > xrpl.nodestore
tests.libxrpl > xrpl.protocol
tests.libxrpl > xrpl.protocol_autogen
tests.libxrpl > xrpl.resource
tests.libxrpl > xrpl.server
tests.libxrpl > xrpl.shamap
tests.libxrpl > xrpl.tx

View File

@@ -11,6 +11,7 @@
#include <limits>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
@@ -233,4 +234,11 @@ makeSlice(std::basic_string<char, Traits, Alloc> const& s)
return Slice(s.data(), s.size());
}
template <class Traits>
Slice
makeSlice(std::basic_string_view<char, Traits> s)
{
return Slice(s.data(), s.size());
}
} // namespace xrpl

View File

@@ -41,7 +41,7 @@ public:
operator=(NodePtr node)
{
node_ = node;
return static_cast<LockFreeStackIterator&>(*this);
return *this;
}
LockFreeStackIterator&

View File

@@ -49,6 +49,11 @@ public:
impl_->set(value);
}
// This is a write-through handle: assignment sets the value of the
// referenced metric. It is const-qualified and returns Gauge const&
// (a non-const Gauge& would require a const_cast), so it does not follow
// the conventional assignment-operator signature.
// NOLINTNEXTLINE(misc-unconventional-assign-operator)
Gauge const&
operator=(value_type value) const
{

View File

@@ -26,9 +26,7 @@ struct Zero
explicit Zero() = default;
};
namespace {
constexpr Zero kZero{};
} // namespace
inline constexpr Zero kZero{};
/** Default implementation of signum calls the method on the class. */
template <typename T>

View File

@@ -246,7 +246,15 @@ message TMGetObjectByHash {
message TMLedgerNode {
required bytes nodedata = 1;
optional bytes nodeid = 2; // missing for ledger base data
// Used when protocol version <2.3. Not set for ledger base data.
optional bytes nodeid = 2;
// Used when protocol version >=2.3. Neither value is set for ledger base data.
oneof reference {
bytes id = 3; // Set for inner nodes.
uint32 depth = 4; // Set for leaf nodes.
}
}
enum TMLedgerInfoType {

View File

@@ -555,7 +555,11 @@ public:
ValueProxy&
operator=(ValueProxy const&) = delete;
// Write-through proxy: assignment sets the referenced field to the given
// value, so it intentionally takes the assigned value rather than a
// ValueProxy.
template <class U>
// NOLINTNEXTLINE(misc-unconventional-assign-operator)
ValueProxy&
operator=(U&& u)
requires(std::is_assignable_v<T, U>);
@@ -800,6 +804,7 @@ STObject::Proxy<T>::assign(U&& u)
template <class T>
template <class U>
// NOLINTNEXTLINE(misc-unconventional-assign-operator)
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator=(U&& u)
requires(std::is_assignable_v<T, U>)

View File

@@ -3,7 +3,6 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
@@ -88,6 +87,17 @@ enum class SHAMapState {
See https://en.wikipedia.org/wiki/Merkle_tree
*/
/** Holds a SHAMap node's identity, serialized data, and leaf status.
Used by getNodeFat to return node data for peer synchronization.
*/
struct SHAMapNodeData
{
SHAMapNodeID nodeID;
bool isLeaf;
Blob data; // Placed last, so `isLeaf` can fit into the alignment padding of `nodeID`.
};
class SHAMap
{
private:
@@ -265,10 +275,10 @@ public:
std::vector<std::pair<SHAMapNodeID, uint256>>
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
bool
[[nodiscard]] bool
getNodeFat(
SHAMapNodeID const& wanted,
std::vector<std::pair<SHAMapNodeID, Blob>>& data,
std::vector<SHAMapNodeData>& data,
bool fatLeaves,
std::uint32_t depth) const;
@@ -295,10 +305,43 @@ public:
void
serializeRoot(Serializer& s) const;
/** Add a root node to the SHAMap during synchronization.
*
* This function is used when receiving the root node of a SHAMap from a peer during ledger
* synchronization. The node must already have been deserialized.
*
* @param hash The expected hash of the root node.
* @param rootNode A deserialized root node to add.
* @param filter Optional sync filter to track received nodes.
* @return Status indicating whether the node was useful, duplicate, or invalid.
*
* @note This function expects the rootNode to be a valid, deserialized SHAMapTreeNode. The
* caller is responsible for deserialization and basic validation before calling this
* function.
*/
SHAMapAddNode
addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter);
addRootNode(SHAMapHash const& hash, SHAMapTreeNodePtr rootNode, SHAMapSyncFilter const* filter);
/** Add a known node at a specific position in the SHAMap during synchronization.
*
* This function is used when receiving nodes from peers during ledger synchronization. The node
* is inserted at the position specified by nodeID. The node must already have been
* deserialized.
*
* @param nodeID The position in the tree where this node belongs.
* @param treeNode A deserialized tree node to add.
* @param filter Optional sync filter to track received nodes.
* @return Status indicating whether the node was useful, duplicate, or invalid.
*
* @note This function expects the treeNode to be a valid, deserialized SHAMapTreeNode. The
* caller is responsible for deserialization and basic validation before calling this
* function. This also means that the nodeID must be consistent with the node's content.
*/
SHAMapAddNode
addKnownNode(SHAMapNodeID const& nodeID, Slice const& rawNode, SHAMapSyncFilter const* filter);
addKnownNode(
SHAMapNodeID const& nodeID,
SHAMapTreeNodePtr treeNode,
SHAMapSyncFilter const* filter);
// status functions
void

View File

@@ -129,7 +129,8 @@ selectBranch(SHAMapNodeID const& id, uint256 const& hash)
SHAMapNodeID
SHAMapNodeID::createID(int depth, uint256 const& key)
{
XRPL_ASSERT((depth >= 0) && (depth < 65), "xrpl::SHAMapNodeID::createID : valid branch input");
XRPL_ASSERT(
depth >= 0 && depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth");
return SHAMapNodeID(depth, key & depthMask(depth));
}

View File

@@ -413,7 +413,7 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
bool
SHAMap::getNodeFat(
SHAMapNodeID const& wanted,
std::vector<std::pair<SHAMapNodeID, Blob>>& data,
std::vector<SHAMapNodeData>& data,
bool fatLeaves,
std::uint32_t depth) const
{
@@ -459,7 +459,7 @@ SHAMap::getNodeFat(
// Add this node to the reply
s.erase();
node->serializeForWire(s);
data.emplace_back(nodeID, s.getData());
data.emplace_back(nodeID, node->isLeaf(), s.getData());
if (node->isInner())
{
@@ -489,7 +489,7 @@ SHAMap::getNodeFat(
// Just include this node
s.erase();
childNode->serializeForWire(s);
data.emplace_back(childID, s.getData());
data.emplace_back(childID, childNode->isLeaf(), s.getData());
}
}
}
@@ -507,25 +507,32 @@ SHAMap::serializeRoot(Serializer& s) const
}
SHAMapAddNode
SHAMap::addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter)
SHAMap::addRootNode(
SHAMapHash const& hash,
SHAMapTreeNodePtr rootNode,
SHAMapSyncFilter const* filter)
{
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
// we already have a root_ node
if (root_->getHash().isNonZero())
{
JLOG(journal_.trace()) << "got root node, already have one";
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash input");
JLOG(journal_.trace()) << "Got root node, already have one";
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash");
return SHAMapAddNode::duplicate();
}
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
auto node = SHAMapTreeNode::makeFromWire(rootNode);
if (!node || node->getHash() != hash)
if (rootNode->getHash() != hash)
{
JLOG(journal_.warn()) << "Corrupt node received";
return SHAMapAddNode::invalid();
}
if (backed_)
canonicalize(hash, node);
canonicalize(hash, rootNode);
root_ = node;
root_ = std::move(rootNode);
if (root_->isLeaf())
clearSynching();
@@ -542,9 +549,20 @@ SHAMap::addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFil
}
SHAMapAddNode
SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncFilter const* filter)
SHAMap::addKnownNode(
SHAMapNodeID const& nodeID,
SHAMapTreeNodePtr treeNode,
SHAMapSyncFilter const* filter)
{
XRPL_ASSERT(!node.isRoot(), "xrpl::SHAMap::addKnownNode : valid node input");
XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node");
XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node");
XRPL_ASSERT(
!treeNode->isLeaf() ||
SHAMapNodeID::createID(
nodeID.getDepth(),
safeDowncast<SHAMapLeafNode const*>(treeNode.get())->peekItem()->key())
.getNodeID() == nodeID.getNodeID(),
"xrpl::SHAMap::addKnownNode : leaf position consistent with node ID");
if (!isSynching())
{
@@ -558,14 +576,14 @@ SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncF
while (currNode->isInner() &&
!safeDowncast<SHAMapInnerNode*>(currNode)->isFullBelow(generation) &&
(currNodeID.getDepth() < node.getDepth()))
(currNodeID.getDepth() < nodeID.getDepth()))
{
int const branch = selectBranch(currNodeID, node.getNodeID());
int const branch = selectBranch(currNodeID, nodeID.getNodeID());
XRPL_ASSERT(branch >= 0, "xrpl::SHAMap::addKnownNode : valid branch");
auto inner = safeDowncast<SHAMapInnerNode*>(currNode);
if (inner->isEmptyBranch(branch))
{
JLOG(journal_.warn()) << "Add known node for empty branch" << node;
JLOG(journal_.warn()) << "Add known node for empty branch" << nodeID;
return SHAMapAddNode::invalid();
}
@@ -581,67 +599,44 @@ SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncF
if (currNode != nullptr)
continue;
auto newNode = SHAMapTreeNode::makeFromWire(rawNode);
if (!newNode || childHash != newNode->getHash())
if (childHash != treeNode->getHash())
{
JLOG(journal_.warn()) << "Corrupt node received";
return SHAMapAddNode::invalid();
}
// In rare cases, a node can still be corrupt even after hash
// validation. For leaf nodes, we perform an additional check to
// ensure the node's position in the tree is consistent with its
// content to prevent inconsistencies that could
// propagate further down the line.
if (newNode->isLeaf())
{
auto const& actualKey =
safeDowncast<SHAMapLeafNode const*>(newNode.get())->peekItem()->key();
// Validate that this leaf belongs at the target position
auto const expectedNodeID = SHAMapNodeID::createID(node.getDepth(), actualKey);
if (expectedNodeID.getNodeID() != node.getNodeID())
{
JLOG(journal_.debug())
<< "Leaf node position mismatch: "
<< "expected=" << expectedNodeID.getNodeID() << ", actual=" << node.getNodeID();
return SHAMapAddNode::invalid();
}
}
// Inner nodes must be at a level strictly less than 64
// but leaf nodes (while notionally at level 64) can be
// at any depth up to and including 64:
if ((currNodeID.getDepth() > kLeafDepth) ||
(newNode->isInner() && currNodeID.getDepth() == kLeafDepth))
(treeNode->isInner() && currNodeID.getDepth() == kLeafDepth))
{
// Map is provably invalid
state_ = SHAMapState::Invalid;
return SHAMapAddNode::useful();
}
if (currNodeID != node)
if (currNodeID != nodeID)
{
// Either this node is broken or we didn't request it (yet)
JLOG(journal_.warn()) << "unable to hook node " << node;
JLOG(journal_.warn()) << "unable to hook node " << nodeID;
JLOG(journal_.info()) << " stuck at " << currNodeID;
JLOG(journal_.info()) << "got depth=" << node.getDepth()
JLOG(journal_.info()) << "got depth=" << nodeID.getDepth()
<< ", walked to= " << currNodeID.getDepth();
return SHAMapAddNode::useful();
}
if (backed_)
canonicalize(childHash, newNode);
canonicalize(childHash, treeNode);
newNode = prevNode->canonicalizeChild(branch, std::move(newNode));
treeNode = prevNode->canonicalizeChild(branch, std::move(treeNode));
if (filter != nullptr)
{
Serializer s;
newNode->serializeWithPrefix(s);
treeNode->serializeWithPrefix(s);
filter->gotNode(
false, childHash, ledgerSeq_, std::move(s.modData()), newNode->getType());
false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType());
}
return SHAMapAddNode::useful();

View File

@@ -0,0 +1,260 @@
#include <xrpld/app/ledger/LedgerNodeHelpers.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapAccountStateLeafNode.h>
#include <xrpl/shamap/SHAMapInnerNode.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <xrpl.pb.h>
#include <cstdint>
#include <string>
namespace xrpl::tests {
class LedgerNodeHelpers_test : public beast::unit_test::Suite
{
static boost::intrusive_ptr<SHAMapItem>
makeTestItem(std::uint32_t seed)
{
Serializer s;
s.add32(seed);
s.add32(seed + 1);
s.add32(seed + 2);
return makeShamapitem(s.getSHA512Half(), s.slice());
}
static std::string
serializeNode(SHAMapTreeNodePtr const& node)
{
Serializer s;
node->serializeForWire(s);
auto const slice = s.slice();
return std::string(slice.begin(), slice.end());
}
void
testGetTreeNode()
{
testcase("getTreeNode");
// Valid: inner node. It must have at least one child for `serializeNode` to work.
{
auto const innerNode = intr_ptr::makeShared<SHAMapInnerNode>(1);
auto const childNode = intr_ptr::makeShared<SHAMapInnerNode>(1);
innerNode->setChild(0, childNode);
auto const innerData = serializeNode(innerNode);
auto const result = getTreeNode(innerData);
BEAST_EXPECT(result && result->isInner());
}
// Valid: leaf node.
{
auto const leafItem = makeTestItem(12345);
auto const leafNode = intr_ptr::makeShared<SHAMapAccountStateLeafNode>(leafItem, 1);
auto const leafData = serializeNode(leafNode);
auto const result = getTreeNode(leafData);
BEAST_EXPECT(result && result->isLeaf());
}
// Invalid: empty data.
{
auto const result = getTreeNode("");
BEAST_EXPECT(!result);
}
// Invalid: garbage data.
{
auto const result = getTreeNode("invalid");
BEAST_EXPECT(!result);
}
// Invalid: truncated data.
{
auto const leafItem = makeTestItem(54321);
auto const leafNode = intr_ptr::makeShared<SHAMapAccountStateLeafNode>(leafItem, 1);
// Truncate the data to trigger an exception in SHAMapTreeNode::makeAccountState when
// the data is used to deserialize the node.
uint256 const tag;
auto const leafData = serializeNode(leafNode).substr(0, tag.kBytes - 1);
auto const result = getTreeNode(leafData);
BEAST_EXPECT(!result);
}
}
void
testGetSHAMapNodeID()
{
testcase("getSHAMapNodeID");
{
// Tests using inner nodes at various depths.
auto const innerNode = intr_ptr::makeShared<SHAMapInnerNode>(1);
auto const childNode = intr_ptr::makeShared<SHAMapInnerNode>(1);
innerNode->setChild(0, childNode);
auto const innerData = serializeNode(innerNode);
// Valid: legacy `nodeid` field at arbitrary depth.
{
auto const innerDepth = 3;
auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{});
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(innerData);
ledgerNode.set_nodeid(innerID.getRawString());
auto const result = getSHAMapNodeID(ledgerNode, *innerNode);
BEAST_EXPECT(result == innerID);
}
// Valid: new `id` field at minimum depth.
{
auto const innerDepth = 0;
auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{});
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(innerData);
ledgerNode.set_id(innerID.getRawString());
auto const result = getSHAMapNodeID(ledgerNode, *innerNode);
BEAST_EXPECT(result == innerID);
}
// Invalid: new `depth` field should not be used for inner nodes.
{
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(innerData);
ledgerNode.set_depth(10);
auto const result = getSHAMapNodeID(ledgerNode, *innerNode);
BEAST_EXPECT(!result);
}
// Invalid: both legacy `nodeid` and new `id` fields set for an inner node.
{
auto const innerDepth = 9;
auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{});
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(innerData);
ledgerNode.set_nodeid(innerID.getRawString());
ledgerNode.set_id(innerID.getRawString());
auto const result = getSHAMapNodeID(ledgerNode, *innerNode);
BEAST_EXPECT(!result);
}
}
{
// Tests using leaf nodes at various depths.
auto const leafItem = makeTestItem(12345);
auto const leafNode = intr_ptr::makeShared<SHAMapAccountStateLeafNode>(leafItem, 1);
auto const leafData = serializeNode(leafNode);
auto const leafKey = leafItem->key();
// Valid: legacy `nodeid` field at arbitrary depth.
{
auto const kLeafDepth = 5;
auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(leafData);
ledgerNode.set_nodeid(leafID.getRawString());
auto const result = getSHAMapNodeID(ledgerNode, *leafNode);
BEAST_EXPECT(result == leafID);
}
// Invalid: new `id` field should not be used for leaf nodes.
{
auto const kLeafDepth = 5;
auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(leafData);
ledgerNode.set_id(leafID.getRawString());
auto const result = getSHAMapNodeID(ledgerNode, *leafNode);
BEAST_EXPECT(!result);
}
// Valid: new `depth` field at minimum depth.
{
auto const kLeafDepth = 0;
auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(leafData);
ledgerNode.set_depth(kLeafDepth);
auto const result = getSHAMapNodeID(ledgerNode, *leafNode);
BEAST_EXPECT(result == leafID);
}
// Valid: new `depth` field at arbitrary depth between minimum and maximum.
{
auto const kLeafDepth = 10;
auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(leafData);
ledgerNode.set_depth(kLeafDepth);
auto const result = getSHAMapNodeID(ledgerNode, *leafNode);
BEAST_EXPECT(result == leafID);
}
// Valid: new `depth` field at maximum depth.
// Note that we do not test a depth greater than the maximum depth, because the proto
// message is assumed to have been validated by the time the getSHAMapNodeID function is
// called.
{
auto const kLeafDepth = SHAMap::kLeafDepth;
auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(leafData);
ledgerNode.set_depth(kLeafDepth);
auto const result = getSHAMapNodeID(ledgerNode, *leafNode);
BEAST_EXPECT(result == leafID);
}
// Invalid: legacy `nodeid` field where the node ID is inconsistent with the key.
{
auto const otherItem = makeTestItem(54321);
auto const otherNode =
intr_ptr::makeShared<SHAMapAccountStateLeafNode>(otherItem, 1);
auto const otherData = serializeNode(otherNode);
auto const otherKey = otherItem->key();
auto const otherDepth = 1;
auto const otherID = SHAMapNodeID::createID(otherDepth, otherKey);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata(otherData);
ledgerNode.set_nodeid(otherID.getRawString());
auto const result = getSHAMapNodeID(ledgerNode, *leafNode);
BEAST_EXPECT(!result);
}
}
// Invalid: no field set.
{
auto const innerNode = intr_ptr::makeShared<SHAMapInnerNode>(1);
protocol::TMLedgerNode ledgerNode;
ledgerNode.set_nodedata("test_data");
auto const result = getSHAMapNodeID(ledgerNode, *innerNode);
BEAST_EXPECT(!result);
}
}
public:
void
run() override
{
testGetTreeNode();
testGetSHAMapNodeID();
}
};
BEAST_DEFINE_TESTSUITE(LedgerNodeHelpers, app, xrpl);
} // namespace xrpl::tests

View File

@@ -28,6 +28,7 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/net/IPAddress.h>
#include <xrpl/beast/net/IPEndpoint.h>
#include <xrpl/beast/unit_test/suite.h>
@@ -452,7 +453,7 @@ struct TestPeerSet : public PeerSet
dropRate = 100;
}
if (((rand() % 100) + 1) <= dropRate)
if (randInt(1, 100) <= dropRate)
return;
switch (type)

View File

@@ -1,268 +0,0 @@
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/unit_test/suite.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
namespace xrpl::test {
struct Buffer_test : beast::unit_test::Suite
{
static bool
sane(Buffer const& b)
{
if (b.empty())
return b.data() == nullptr;
return b.data() != nullptr;
}
void
run() override
{
std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23,
0x71, 0x6d, 0x2a, 0x18, 0xb4, 0x70, 0xcb, 0xf5,
0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, 0xf0, 0x2c,
0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3};
Buffer const b0;
BEAST_EXPECT(sane(b0));
BEAST_EXPECT(b0.empty());
Buffer b1{0};
BEAST_EXPECT(sane(b1));
BEAST_EXPECT(b1.empty());
std::memcpy(b1.alloc(16), data, 16);
BEAST_EXPECT(sane(b1));
BEAST_EXPECT(!b1.empty());
BEAST_EXPECT(b1.size() == 16);
Buffer b2{b1.size()};
BEAST_EXPECT(sane(b2));
BEAST_EXPECT(!b2.empty());
BEAST_EXPECT(b2.size() == b1.size());
std::memcpy(b2.data(), data + 16, 16);
Buffer b3{data, sizeof(data)};
BEAST_EXPECT(sane(b3));
BEAST_EXPECT(!b3.empty());
BEAST_EXPECT(b3.size() == sizeof(data));
BEAST_EXPECT(std::memcmp(b3.data(), data, b3.size()) == 0);
// Check equality and inequality comparisons
BEAST_EXPECT(b0 == b0);
BEAST_EXPECT(b0 != b1);
BEAST_EXPECT(b1 == b1);
BEAST_EXPECT(b1 != b2);
BEAST_EXPECT(b2 != b3);
// Check copy constructors and copy assignments:
{
testcase("Copy Construction / Assignment");
Buffer x{b0};
BEAST_EXPECT(x == b0);
BEAST_EXPECT(sane(x));
Buffer y{b1};
BEAST_EXPECT(y == b1);
BEAST_EXPECT(sane(y));
x = b2;
BEAST_EXPECT(x == b2);
BEAST_EXPECT(sane(x));
x = y;
BEAST_EXPECT(x == y);
BEAST_EXPECT(sane(x));
y = b3;
BEAST_EXPECT(y == b3);
BEAST_EXPECT(sane(y));
x = b0;
BEAST_EXPECT(x == b0);
BEAST_EXPECT(sane(x));
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
#endif
x = x;
BEAST_EXPECT(x == b0);
BEAST_EXPECT(sane(x));
y = y;
BEAST_EXPECT(y == b3);
BEAST_EXPECT(sane(y));
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
}
// Check move constructor & move assignments:
{
testcase("Move Construction / Assignment");
static_assert(std::is_nothrow_move_constructible_v<Buffer>);
static_assert(std::is_nothrow_move_assignable_v<Buffer>);
{ // Move-construct from empty buf
Buffer x;
Buffer const y{std::move(x)};
BEAST_EXPECT(sane(x)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(x.empty()); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(sane(y));
BEAST_EXPECT(y.empty());
BEAST_EXPECT(x == y); // NOLINT(bugprone-use-after-move)
}
{ // Move-construct from non-empty buf
Buffer x{b1};
Buffer const y{std::move(x)};
BEAST_EXPECT(sane(x)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(x.empty()); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(sane(y));
BEAST_EXPECT(y == b1);
}
{ // Move assign empty buf to empty buf
Buffer x;
Buffer y;
x = std::move(y);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x.empty());
BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign non-empty buf to empty buf
Buffer x;
Buffer y{b1};
x = std::move(y);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x == b1);
BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign empty buf to non-empty buf
Buffer x{b1};
Buffer y;
x = std::move(y);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x.empty());
BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign non-empty buf to non-empty buf
Buffer x{b1};
Buffer y{b2};
Buffer z{b3};
x = std::move(y);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(!x.empty());
BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move)
x = std::move(z);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(!x.empty());
BEAST_EXPECT(sane(z)); // NOLINT(bugprone-use-after-move)
BEAST_EXPECT(z.empty()); // NOLINT(bugprone-use-after-move)
}
}
{
testcase("Slice Conversion / Construction / Assignment");
Buffer w{static_cast<Slice>(b0)};
BEAST_EXPECT(sane(w));
BEAST_EXPECT(w == b0);
Buffer x{static_cast<Slice>(b1)};
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x == b1);
Buffer y{static_cast<Slice>(b2)};
BEAST_EXPECT(sane(y));
BEAST_EXPECT(y == b2);
Buffer z{static_cast<Slice>(b3)};
BEAST_EXPECT(sane(z));
BEAST_EXPECT(z == b3);
// Assign empty slice to empty buffer
w = static_cast<Slice>(b0);
BEAST_EXPECT(sane(w));
BEAST_EXPECT(w == b0);
// Assign non-empty slice to empty buffer
w = static_cast<Slice>(b1);
BEAST_EXPECT(sane(w));
BEAST_EXPECT(w == b1);
// Assign non-empty slice to non-empty buffer
x = static_cast<Slice>(b2);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x == b2);
// Assign non-empty slice to non-empty buffer
y = static_cast<Slice>(z);
BEAST_EXPECT(sane(y));
BEAST_EXPECT(y == z);
// Assign empty slice to non-empty buffer:
z = static_cast<Slice>(b0);
BEAST_EXPECT(sane(z));
BEAST_EXPECT(z == b0);
}
{
testcase("Allocation, Deallocation and Clearing");
auto test = [this](Buffer const& b, std::size_t i) {
Buffer x{b};
// Try to allocate some number of bytes, possibly
// zero (which means clear) and sanity check
x(i);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x.size() == i);
BEAST_EXPECT((x.data() == nullptr) == (i == 0));
// Try to allocate some more data (always non-zero)
x(i + 1);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x.size() == i + 1);
BEAST_EXPECT(x.data() != nullptr);
// Try to clear:
x.clear();
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x.empty());
BEAST_EXPECT(x.data() == nullptr);
// Try to clear again:
x.clear();
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x.empty());
BEAST_EXPECT(x.data() == nullptr);
};
for (std::size_t i = 0; i < 16; ++i)
{
test(b0, i);
test(b1, i);
}
}
}
};
BEAST_DEFINE_TESTSUITE(Buffer, basics, xrpl);
} // namespace xrpl::test

View File

@@ -1,64 +0,0 @@
#include <test/unit_test/FileDirGuard.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/beast/unit_test/suite.h>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
namespace xrpl {
class FileUtilities_test : public beast::unit_test::Suite
{
public:
void
testGetFileContents()
{
using namespace xrpl::detail;
using namespace boost::system;
static constexpr char const* kExpectedContents =
"This file is very short. That's all we need.";
FileDirGuard const file(
*this, "test_file", "test.txt", "This is temporary text that should get overwritten");
error_code ec;
auto const path = file.file();
writeFileContents(ec, path, kExpectedContents);
BEAST_EXPECT(!ec);
{
// Test with no max
auto const good = getFileContents(ec, path);
BEAST_EXPECT(!ec);
BEAST_EXPECT(good == kExpectedContents);
}
{
// Test with large max
auto const good = getFileContents(ec, path, kilobytes(1));
BEAST_EXPECT(!ec);
BEAST_EXPECT(good == kExpectedContents);
}
{
// Test with small max
auto const bad = getFileContents(ec, path, 16);
BEAST_EXPECT(ec && ec.value() == boost::system::errc::file_too_large);
BEAST_EXPECT(bad.empty());
}
}
void
run() override
{
testGetFileContents();
}
};
BEAST_DEFINE_TESTSUITE(FileUtilities, basics, xrpl);
} // namespace xrpl

View File

@@ -1,276 +0,0 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/IOUAmount.h>
#include <cstdint>
#include <limits>
#include <sstream>
namespace xrpl {
class IOUAmount_test : public beast::unit_test::Suite
{
public:
void
testZero()
{
testcase("zero");
IOUAmount const z(0, 0);
BEAST_EXPECT(z.mantissa() == 0);
BEAST_EXPECT(z.exponent() == -100);
BEAST_EXPECT(!z);
BEAST_EXPECT(z.signum() == 0);
BEAST_EXPECT(z == beast::kZero);
BEAST_EXPECT((z + z) == z);
BEAST_EXPECT((z - z) == z);
BEAST_EXPECT(z == -z);
IOUAmount const zz(beast::kZero);
BEAST_EXPECT(z == zz);
// https://github.com/XRPLF/rippled/issues/5170
IOUAmount const zzz{};
BEAST_EXPECT(zzz == beast::kZero);
// BEAST_EXPECT(zzz == zz);
}
void
testSigNum()
{
testcase("signum");
IOUAmount const neg(-1, 0);
BEAST_EXPECT(neg.signum() < 0);
IOUAmount const zer(0, 0);
BEAST_EXPECT(zer.signum() == 0);
IOUAmount const pos(1, 0);
BEAST_EXPECT(pos.signum() > 0);
}
void
testBeastZero()
{
testcase("beast::Zero Comparisons");
using beast::kZero;
{
IOUAmount const z(kZero);
BEAST_EXPECT(z == kZero);
BEAST_EXPECT(z >= kZero);
BEAST_EXPECT(z <= kZero);
unexpected(z != kZero);
unexpected(z > kZero);
unexpected(z < kZero);
}
{
IOUAmount const neg(-2, 0);
BEAST_EXPECT(neg < kZero);
BEAST_EXPECT(neg <= kZero);
BEAST_EXPECT(neg != kZero);
unexpected(neg == kZero);
}
{
IOUAmount const pos(2, 0);
BEAST_EXPECT(pos > kZero);
BEAST_EXPECT(pos >= kZero);
BEAST_EXPECT(pos != kZero);
unexpected(pos == kZero);
}
}
void
testComparisons()
{
testcase("IOU Comparisons");
IOUAmount const n(-2, 0);
IOUAmount const z(0, 0);
IOUAmount const p(2, 0);
BEAST_EXPECT(z == z);
BEAST_EXPECT(z >= z);
BEAST_EXPECT(z <= z);
BEAST_EXPECT(z == -z);
// NOLINTBEGIN(misc-redundant-expression)
unexpected(z > z);
unexpected(z < z);
unexpected(z != z);
// NOLINTEND(misc-redundant-expression)
unexpected(z != -z);
BEAST_EXPECT(n < z);
BEAST_EXPECT(n <= z);
BEAST_EXPECT(n != z);
unexpected(n > z);
unexpected(n >= z);
unexpected(n == z);
BEAST_EXPECT(p > z);
BEAST_EXPECT(p >= z);
BEAST_EXPECT(p != z);
unexpected(p < z);
unexpected(p <= z);
unexpected(p == z);
BEAST_EXPECT(n < p);
BEAST_EXPECT(n <= p);
BEAST_EXPECT(n != p);
unexpected(n > p);
unexpected(n >= p);
unexpected(n == p);
BEAST_EXPECT(p > n);
BEAST_EXPECT(p >= n);
BEAST_EXPECT(p != n);
unexpected(p < n);
unexpected(p <= n);
unexpected(p == n);
BEAST_EXPECT(p > -p);
BEAST_EXPECT(p >= -p);
BEAST_EXPECT(p != -p);
BEAST_EXPECT(n < -n);
BEAST_EXPECT(n <= -n);
BEAST_EXPECT(n != -n);
}
void
testToString()
{
testcase("IOU strings");
auto test = [this](IOUAmount const& n, std::string const& expected) {
auto const result = to_string(n);
std::stringstream ss;
ss << "to_string(" << result << "). Expected: " << expected;
BEAST_EXPECTS(result == expected, ss.str());
};
for (auto const mantissaSize : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg(mantissaSize);
test(IOUAmount(-2, 0), "-2");
test(IOUAmount(0, 0), "0");
test(IOUAmount(2, 0), "2");
test(IOUAmount(25, -3), "0.025");
test(IOUAmount(-25, -3), "-0.025");
test(IOUAmount(25, 1), "250");
test(IOUAmount(-25, 1), "-250");
test(IOUAmount(2, 20), "2e20");
test(IOUAmount(-2, -20), "-2e-20");
}
}
void
testMulRatio()
{
testcase("mulRatio");
/* The range for the mantissa when normalized */
static constexpr std::int64_t kMinMantissa = 1000000000000000ull;
static constexpr std::int64_t kMaxMantissa = 9999999999999999ull;
// log(2,maxMantissa) ~ 53.15
/* The range for the exponent when normalized */
static constexpr int kMinExponent = -96;
static constexpr int kMaxExponent = 80;
constexpr auto kMaxUInt = std::numeric_limits<std::uint32_t>::max();
{
// multiply by a number that would overflow the mantissa, then
// divide by the same number, and check we didn't lose any value
IOUAmount const bigMan(kMaxMantissa, 0);
BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, true));
// rounding mode shouldn't matter as the result is exact
BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, false));
}
{
// Similar test as above, but for negative values
IOUAmount const bigMan(-kMaxMantissa, 0);
BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, true));
// rounding mode shouldn't matter as the result is exact
BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, false));
}
{
// small amounts
IOUAmount const tiny(kMinMantissa, kMinExponent);
// Round up should give the smallest allowable number
BEAST_EXPECT(tiny == mulRatio(tiny, 1, kMaxUInt, true));
BEAST_EXPECT(tiny == mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true));
// rounding down should be zero
BEAST_EXPECT(beast::kZero == mulRatio(tiny, 1, kMaxUInt, false));
BEAST_EXPECT(beast::kZero == mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false));
// tiny negative numbers
IOUAmount const tinyNeg(-kMinMantissa, kMinExponent);
// Round up should give zero
BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, 1, kMaxUInt, true));
BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true));
// rounding down should be tiny
BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, 1, kMaxUInt, false));
BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false));
}
{ // rounding
{
IOUAmount const one(1, 0);
auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false);
BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1);
}
{
IOUAmount const big(kMaxMantissa, kMaxExponent);
auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false);
BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1);
}
{
IOUAmount const negOne(-1, 0);
auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false);
BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1);
}
}
{
// division by zero
IOUAmount one(1, 0);
except([&] { mulRatio(one, 1, 0, true); });
}
{
// overflow
IOUAmount big(kMaxMantissa, kMaxExponent);
except([&] { mulRatio(big, 2, 0, true); });
}
} // namespace xrpl
//--------------------------------------------------------------------------
void
run() override
{
testZero();
testSigNum();
testBeastZero();
testComparisons();
testToString();
testMulRatio();
}
};
BEAST_DEFINE_TESTSUITE(IOUAmount, basics, xrpl);
} // namespace xrpl

View File

@@ -1,879 +0,0 @@
#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/beast/unit_test/suite.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <chrono> // IWYU pragma: keep
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <latch>
#include <mutex>
#include <optional>
#include <random>
#include <string>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
namespace xrpl::tests {
/**
Experimentally, we discovered that using std::barrier performs extremely
poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS
environments. To unblock our macOS CI pipeline, we replaced std::barrier with a
custom mutex-based barrier (Barrier) that significantly improves performance
without compromising correctness. For future reference, if we ever consider
reintroducing std::barrier, the following configuration is known to exhibit the
problem:
Model Name: Mac mini
Model Identifier: Mac14,3
Model Number: Z16K000R4LL/A
Chip: Apple M2
Total Number of Cores: 8 (4 performance and 4 efficiency)
Memory: 24 GB
System Firmware Version: 11881.41.5
OS Loader Version: 11881.1.1
Apple clang version 16.0.0 (clang-1600.0.26.3)
Target: arm64-apple-darwin24.0.0
Thread model: posix
*/
struct Barrier
{
std::mutex mtx;
std::condition_variable cv;
int count;
int const initial;
Barrier(int n) : count(n), initial(n)
{
}
void
arriveAndWait()
{
std::unique_lock lock(mtx);
if (--count == 0)
{
count = initial;
cv.notify_all();
}
else
{
cv.wait(lock, [&] { return count == initial; });
}
}
};
namespace {
enum class TrackedState : std::uint8_t {
Uninitialized,
Alive,
PartiallyDeletedStarted,
PartiallyDeleted,
DeletedStarted,
Deleted
};
class TIBase : public IntrusiveRefCounts
{
public:
static constexpr std::size_t kMaxStates = 128;
static std::array<std::atomic<TrackedState>, kMaxStates> state;
static std::atomic<int> nextId;
static TrackedState
getState(int id)
{
assert(id < state.size());
return state[id].load(std::memory_order_acquire);
}
static void
resetStates(bool resetCallback)
{
for (int i = 0; i < kMaxStates; ++i)
{
state[i].store(TrackedState::Uninitialized, std::memory_order_release);
}
nextId.store(0, std::memory_order_release);
if (resetCallback)
TIBase::tracingCallback = [](TrackedState, std::optional<TrackedState>) {};
}
struct ResetStatesGuard
{
bool resetCallback{false};
ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback}
{
TIBase::resetStates(resetCallback);
}
~ResetStatesGuard()
{
TIBase::resetStates(resetCallback);
}
};
TIBase() : id{checkoutID()}
{
assert(state.size() > id);
state[id].store(TrackedState::Alive, std::memory_order_relaxed);
}
~TIBase() override
{
using enum TrackedState;
assert(state.size() > id);
tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted);
assert(state.size() > id);
// Use relaxed memory order to try to avoid atomic operations from
// adding additional memory synchronizations that may hide threading
// errors in the underlying shared pointer class.
state[id].store(DeletedStarted, std::memory_order_relaxed);
tracingCallback(DeletedStarted, Deleted);
assert(state.size() > id);
state[id].store(TrackedState::Deleted, std::memory_order_relaxed);
tracingCallback(TrackedState::Deleted, std::nullopt);
}
void
partialDestructor() const
{
using enum TrackedState;
assert(state.size() > id);
tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted);
assert(state.size() > id);
state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed);
tracingCallback(PartiallyDeletedStarted, PartiallyDeleted);
assert(state.size() > id);
state[id].store(PartiallyDeleted, std::memory_order_relaxed);
tracingCallback(PartiallyDeleted, std::nullopt);
}
static std::function<void(TrackedState, std::optional<TrackedState>)> tracingCallback;
int id;
private:
static int
checkoutID()
{
return nextId.fetch_add(1, std::memory_order_acq_rel);
}
};
std::array<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
std::atomic<int> TIBase::nextId{0};
std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
[](TrackedState, std::optional<TrackedState>) {};
} // namespace
class IntrusiveShared_test : public beast::unit_test::Suite
{
public:
void
testBasics()
{
testcase("Basics");
{
TIBase::ResetStatesGuard const rsg{true};
TIBase const b;
BEAST_EXPECT(b.useCount() == 1);
b.addWeakRef();
BEAST_EXPECT(b.useCount() == 1);
auto s = b.releaseStrongRef();
BEAST_EXPECT(s == ReleaseStrongRefAction::PartialDestroy);
BEAST_EXPECT(b.useCount() == 0);
TIBase const* pb = &b;
partialDestructorFinished(&pb);
BEAST_EXPECT(!pb);
auto w = b.releaseWeakRef();
BEAST_EXPECT(w == ReleaseWeakRefAction::Destroy);
}
std::vector<SharedIntrusive<TIBase>> strong;
std::vector<WeakIntrusive<TIBase>> weak;
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
auto id = b->id;
BEAST_EXPECT(TIBase::getState(id) == Alive);
BEAST_EXPECT(b->useCount() == 1);
for (int i = 0; i < 10; ++i)
{
strong.push_back(b);
}
b.reset();
BEAST_EXPECT(TIBase::getState(id) == Alive);
strong.resize(strong.size() - 1);
BEAST_EXPECT(TIBase::getState(id) == Alive);
strong.clear();
BEAST_EXPECT(TIBase::getState(id) == Deleted);
b = makeSharedIntrusive<TIBase>();
id = b->id;
BEAST_EXPECT(TIBase::getState(id) == Alive);
BEAST_EXPECT(b->useCount() == 1);
for (int i = 0; i < 10; ++i)
{
weak.emplace_back(b);
BEAST_EXPECT(b->useCount() == 1);
}
BEAST_EXPECT(TIBase::getState(id) == Alive);
weak.resize(weak.size() - 1);
BEAST_EXPECT(TIBase::getState(id) == Alive);
b.reset();
BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
while (!weak.empty())
{
weak.resize(weak.size() - 1);
if (!weak.empty())
BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
}
BEAST_EXPECT(TIBase::getState(id) == Deleted);
}
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
auto id = b->id;
BEAST_EXPECT(TIBase::getState(id) == Alive);
WeakIntrusive<TIBase> w{b};
BEAST_EXPECT(TIBase::getState(id) == Alive);
auto s = w.lock();
BEAST_EXPECT(s && s->useCount() == 2);
b.reset();
BEAST_EXPECT(TIBase::getState(id) == Alive);
BEAST_EXPECT(s && s->useCount() == 1);
s.reset();
BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
BEAST_EXPECT(w.expired());
s = w.lock();
// Cannot convert a weak pointer to a strong pointer if object is
// already partially deleted
BEAST_EXPECT(!s);
w.reset();
BEAST_EXPECT(TIBase::getState(id) == Deleted);
}
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
using swu = SharedWeakUnion<TIBase>;
swu b = makeSharedIntrusive<TIBase>();
BEAST_EXPECT(b.isStrong() && b.useCount() == 1);
auto id = b.get()->id;
BEAST_EXPECT(TIBase::getState(id) == Alive);
swu w = b;
BEAST_EXPECT(TIBase::getState(id) == Alive);
BEAST_EXPECT(w.isStrong() && b.useCount() == 2);
w.convertToWeak();
BEAST_EXPECT(w.isWeak() && b.useCount() == 1);
swu s = w;
BEAST_EXPECT(s.isWeak() && b.useCount() == 1);
s.convertToStrong();
BEAST_EXPECT(s.isStrong() && b.useCount() == 2);
b.reset();
BEAST_EXPECT(TIBase::getState(id) == Alive);
BEAST_EXPECT(s.useCount() == 1);
BEAST_EXPECT(!w.expired());
s.reset();
BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted);
BEAST_EXPECT(w.expired());
w.convertToStrong();
// Cannot convert a weak pointer to a strong pointer if object is
// already partially deleted
BEAST_EXPECT(w.isWeak());
w.reset();
BEAST_EXPECT(TIBase::getState(id) == Deleted);
}
{
// Testing SharedWeakUnion assignment operator
TIBase::ResetStatesGuard const rsg{true};
auto strong1 = makeSharedIntrusive<TIBase>();
auto strong2 = makeSharedIntrusive<TIBase>();
auto id1 = strong1->id;
auto id2 = strong2->id;
BEAST_EXPECT(id1 != id2);
SharedWeakUnion<TIBase> union1 = strong1;
SharedWeakUnion<TIBase> union2 = strong2;
BEAST_EXPECT(union1.isStrong());
BEAST_EXPECT(union2.isStrong());
BEAST_EXPECT(union1.get() == strong1.get());
BEAST_EXPECT(union2.get() == strong2.get());
// 1) Normal assignment: explicitly calls SharedWeakUnion assignment
union1 = union2;
BEAST_EXPECT(union1.isStrong());
BEAST_EXPECT(union2.isStrong());
BEAST_EXPECT(union1.get() == union2.get());
BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive);
BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Alive);
// 2) Test self-assignment
BEAST_EXPECT(union1.isStrong());
BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive);
int const initialRefCount = strong1->useCount();
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
union1 = union1; // Self-assignment
#pragma clang diagnostic pop
BEAST_EXPECT(union1.isStrong());
BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive);
BEAST_EXPECT(strong1->useCount() == initialRefCount);
// 3) Test assignment from null union pointer
union1 = SharedWeakUnion<TIBase>();
BEAST_EXPECT(union1.get() == nullptr);
// 4) Test assignment to expired union pointer
strong2.reset();
union2.reset();
union1 = union2;
BEAST_EXPECT(union1.get() == nullptr);
BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Deleted);
}
}
void
testPartialDelete()
{
testcase("Partial Delete");
// This test creates two threads. One with a strong pointer and one
// with a weak pointer. The strong pointer is reset while the weak
// pointer still holds a reference, triggering a partial delete.
// While the partial delete function runs (a sleep is inserted) the
// weak pointer is reset. The destructor should wait to run until
// after the partial delete function has completed running.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
auto strong = makeSharedIntrusive<TIBase>();
WeakIntrusive<TIBase> weak{strong};
bool destructorRan = false;
bool partialDeleteRan = false;
std::latch partialDeleteStartedSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
if (next == DeletedStarted)
{
// strong goes out of scope while weak is still in scope
// This checks that partialDelete has run to completion
// before the destructor is called. A sleep is inserted
// inside the partial delete to make sure the destructor is
// given an opportunity to run during partial delete.
BEAST_EXPECT(cur == PartiallyDeleted);
}
if (next == PartiallyDeletedStarted)
{
partialDeleteStartedSyncPoint.arrive_and_wait();
using namespace std::chrono_literals;
// Sleep and let the weak pointer go out of scope,
// potentially triggering a destructor while partial delete
// is running. The test is to make sure that doesn't happen.
std::this_thread::sleep_for(800ms);
}
if (next == PartiallyDeleted)
{
BEAST_EXPECT(!partialDeleteRan && !destructorRan);
partialDeleteRan = true;
}
if (next == Deleted)
{
BEAST_EXPECT(!destructorRan);
destructorRan = true;
}
};
std::thread t1{[&] {
partialDeleteStartedSyncPoint.arrive_and_wait();
weak.reset(); // Trigger a full delete as soon as the partial
// delete starts
}};
std::thread t2{[&] {
strong.reset(); // Trigger a partial delete
}};
t1.join();
t2.join();
BEAST_EXPECT(destructorRan && partialDeleteRan);
}
void
testDestructor()
{
testcase("Destructor");
// This test creates two threads. One with a strong pointer and one
// with a weak pointer. The weak pointer is reset while the strong
// pointer still holds a reference. Then the strong pointer is
// reset. Only the destructor should run. The partial destructor
// should not be called. Since the weak reset runs to completion
// before the strong pointer is reset, threading doesn't add much to
// this test, but there is no harm in keeping it.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
auto strong = makeSharedIntrusive<TIBase>();
WeakIntrusive<TIBase> weak{strong};
bool destructorRan = false;
bool partialDeleteRan = false;
std::latch weakResetSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
if (next == PartiallyDeleted)
{
BEAST_EXPECT(!partialDeleteRan && !destructorRan);
partialDeleteRan = true;
}
if (next == Deleted)
{
BEAST_EXPECT(!destructorRan);
destructorRan = true;
}
};
std::thread t1{[&] {
weak.reset();
weakResetSyncPoint.arrive_and_wait();
}};
std::thread t2{[&] {
weakResetSyncPoint.arrive_and_wait();
strong.reset(); // Trigger a partial delete
}};
t1.join();
t2.join();
BEAST_EXPECT(destructorRan && !partialDeleteRan);
}
void
testMultithreadedClearMixedVariant()
{
testcase("Multithreaded Clear Mixed Variant");
// This test creates and destroys many strong and weak pointers in a
// loop. There is a random mix of strong and weak pointers stored in
// a vector (held as a variant). Both threads clear all the pointers
// and check that the invariants hold.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
BEAST_EXPECT(!partialDeleteRan && !destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
BEAST_EXPECT(!destructorRan);
setDestructorRan();
}
};
auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng)
-> std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> {
std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> result;
std::uniform_int_distribution<> toCreateDist(4, 64);
std::uniform_int_distribution<> isStrongDist(0, 1);
auto numToCreate = toCreateDist(eng);
result.reserve(numToCreate);
for (int i = 0; i < numToCreate; ++i)
{
if (isStrongDist(eng))
{
result.emplace_back(SharedIntrusive<TIBase>(toClone));
}
else
{
result.emplace_back(WeakIntrusive<TIBase>(toClone));
}
}
return result;
};
static constexpr int kLoopIters = 2 * 1024;
static constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> result;
result.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
result.emplace_back(rd());
return result;
}();
// cloneAndDestroy clones the strong pointer into a vector of mixed
// strong and weak pointers and destroys them all at once.
// threadId==0 is special.
auto cloneAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// Thread 0 is the genesis thread. It creates the strong
// pointers to be cloned by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
BEAST_EXPECT(!i || destructorRan);
destructionState.store(0, std::memory_order_release);
toClone.clear();
toClone.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toClone, strong);
}
// ------ Sync Point ------
postCreateToCloneSyncPoint.arriveAndWait();
auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
toClone[threadId].reset();
// ------ Sync Point ------
postCreateVecOfPointersSyncPoint.arriveAndWait();
v.clear();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(cloneAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
void
testMultithreadedClearMixedUnion()
{
testcase("Multithreaded Clear Mixed Union");
// This test creates and destroys many SharedWeak pointers in a
// loop. All the pointers start as strong and a loop randomly
// convert them between strong and weak pointers. Both threads clear
// all the pointers and check that the invariants hold.
//
// Note: This test also differs from the test above in that the pointers
// randomly change from strong to weak and from weak to strong in a
// loop. This can't be done in the variant test above because variant is
// not thread safe while the SharedWeakUnion is thread safe.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
BEAST_EXPECT(!partialDeleteRan && !destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
BEAST_EXPECT(!destructorRan);
setDestructorRan();
}
};
auto createVecOfPointers =
[&](auto const& toClone,
std::default_random_engine& eng) -> std::vector<SharedWeakUnion<TIBase>> {
std::vector<SharedWeakUnion<TIBase>> result;
std::uniform_int_distribution<> toCreateDist(4, 64);
auto numToCreate = toCreateDist(eng);
result.reserve(numToCreate);
for (int i = 0; i < numToCreate; ++i)
result.emplace_back(SharedIntrusive<TIBase>(toClone));
return result;
};
static constexpr int kLoopIters = 2 * 1024;
static constexpr int kFlipPointersLoopIters = 256;
static constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
Barrier postFlipPointersLoopSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> result;
result.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
result.emplace_back(rd());
return result;
}();
// cloneAndDestroy clones the strong pointer into a vector of
// mixed strong and weak pointers, runs a loop that randomly
// changes strong pointers to weak pointers, and destroys them
// all at once.
auto cloneAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// threadId 0 is the genesis thread. It creates the
// strong point to be cloned by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
BEAST_EXPECT(!i || destructorRan);
destructionState.store(0, std::memory_order_release);
toClone.clear();
toClone.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toClone, strong);
}
// ------ Sync Point ------
postCreateToCloneSyncPoint.arriveAndWait();
auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
toClone[threadId].reset();
// ------ Sync Point ------
postCreateVecOfPointersSyncPoint.arriveAndWait();
std::uniform_int_distribution<> isStrongDist(0, 1);
for (int f = 0; f < kFlipPointersLoopIters; ++f)
{
for (auto& p : v)
{
if (isStrongDist(engines[threadId]))
{
p.convertToStrong();
}
else
{
p.convertToWeak();
}
}
}
// ------ Sync Point ------
postFlipPointersLoopSyncPoint.arriveAndWait();
v.clear();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(cloneAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
void
testMultithreadedLockingWeak()
{
testcase("Multithreaded Locking Weak");
// This test creates a single shared atomic pointer that multiple thread
// create weak pointers from. The threads then lock the weak pointers.
// Both threads clear all the pointers and check that the invariants
// hold.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
BEAST_EXPECT(!partialDeleteRan && !destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
BEAST_EXPECT(!destructorRan);
setDestructorRan();
}
};
static constexpr int kLoopIters = 2 * 1024;
static constexpr int kLockWeakLoopIters = 256;
static constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toLock;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToLockSyncPoint{kNumThreads};
Barrier postLockWeakLoopSyncPoint{kNumThreads};
// lockAndDestroy creates weak pointers from the strong pointer
// and runs a loop that locks the weak pointer. At the end of the loop
// all the pointers are destroyed all at once.
auto lockAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// threadId 0 is the genesis thread. It creates the
// strong point to be locked by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
BEAST_EXPECT(!i || destructorRan);
destructionState.store(0, std::memory_order_release);
toLock.clear();
toLock.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toLock, strong);
}
// ------ Sync Point ------
postCreateToLockSyncPoint.arriveAndWait();
// Multiple threads all create a weak pointer from the same
// strong pointer
WeakIntrusive const weak{toLock[threadId]};
for (int wi = 0; wi < kLockWeakLoopIters; ++wi)
{
BEAST_EXPECT(!weak.expired());
auto strong = weak.lock();
BEAST_EXPECT(strong);
}
// ------ Sync Point ------
postLockWeakLoopSyncPoint.arriveAndWait();
toLock[threadId].reset();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(lockAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
void
run() override
{
testBasics();
testPartialDelete();
testDestructor();
testMultithreadedClearMixedVariant();
testMultithreadedClearMixedUnion();
testMultithreadedLockingWeak();
}
}; // namespace tests
BEAST_DEFINE_TESTSUITE(IntrusiveShared, basics, xrpl);
} // namespace xrpl::tests

View File

@@ -1,82 +0,0 @@
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Protocol.h>
#include <string>
namespace xrpl {
class KeyCache_test : public beast::unit_test::Suite
{
public:
void
run() override
{
using namespace std::chrono_literals;
TestStopwatch clock;
clock.set(0);
using Key = std::string;
using Cache = TaggedCache<Key, int, true>;
test::SuiteJournal j("KeyCacheTest", *this);
// Insert an item, retrieve it, and age it so it gets purged.
{
Cache c("test", LedgerIndex(1), 2s, clock, j);
BEAST_EXPECT(c.size() == 0);
BEAST_EXPECT(c.insert("one"));
BEAST_EXPECT(!c.insert("one"));
BEAST_EXPECT(c.size() == 1);
BEAST_EXPECT(c.touchIfExists("one"));
++clock;
c.sweep();
BEAST_EXPECT(c.size() == 1);
++clock;
c.sweep();
BEAST_EXPECT(c.size() == 0);
BEAST_EXPECT(!c.touchIfExists("one"));
}
// Insert two items, have one expire
{
Cache c("test", LedgerIndex(2), 2s, clock, j);
BEAST_EXPECT(c.insert("one"));
BEAST_EXPECT(c.size() == 1);
BEAST_EXPECT(c.insert("two"));
BEAST_EXPECT(c.size() == 2);
++clock;
c.sweep();
BEAST_EXPECT(c.size() == 2);
BEAST_EXPECT(c.touchIfExists("two"));
++clock;
c.sweep();
BEAST_EXPECT(c.size() == 1);
}
// Insert three items (1 over limit), sweep
{
Cache c("test", LedgerIndex(2), 3s, clock, j);
BEAST_EXPECT(c.insert("one"));
++clock;
BEAST_EXPECT(c.insert("two"));
++clock;
BEAST_EXPECT(c.insert("three"));
++clock;
BEAST_EXPECT(c.size() == 3);
c.sweep();
BEAST_EXPECT(c.size() < 3);
}
}
};
BEAST_DEFINE_TESTSUITE(KeyCache, basics, xrpl);
} // namespace xrpl

View File

@@ -1,309 +0,0 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/ToString.h>
#include <xrpl/beast/unit_test/suite.h>
#include <string>
namespace xrpl {
class StringUtilities_test : public beast::unit_test::Suite
{
public:
void
testUnHexSuccess(std::string const& strIn, std::string const& strExpected)
{
auto rv = strUnHex(strIn);
BEAST_EXPECT(rv);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
BEAST_EXPECT(makeSlice(*rv) == makeSlice(strExpected));
}
void
testUnHexFailure(std::string const& strIn)
{
auto rv = strUnHex(strIn);
BEAST_EXPECT(!rv);
}
void
testUnHex()
{
testcase("strUnHex");
testUnHexSuccess("526970706c6544", "RippleD");
testUnHexSuccess("A", "\n");
testUnHexSuccess("0A", "\n");
testUnHexSuccess("D0A", "\r\n");
testUnHexSuccess("0D0A", "\r\n");
testUnHexSuccess("200D0A", " \r\n");
testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)");
// Check for things which contain some or only invalid characters
testUnHexFailure("123X");
testUnHexFailure("V");
testUnHexFailure("XRP");
}
void
testParseUrl()
{
testcase("parseUrl");
// Expected passes.
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain.empty());
BEAST_EXPECT(!pUrl.port);
// RFC 3986:
// > In general, a URI that uses the generic syntax for authority
// with an empty path should be normalized to a path of "/".
// Do we want to normalize paths?
BEAST_EXPECT(pUrl.path.empty());
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme:///"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain.empty());
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "lower://domain"));
BEAST_EXPECT(pUrl.scheme == "lower");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path.empty());
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "UPPER://domain:234/"));
BEAST_EXPECT(pUrl.scheme == "upper");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(*pUrl.port == 234); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT(pUrl.path == "/");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "Mixed://domain/path"));
BEAST_EXPECT(pUrl.scheme == "mixed");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/path");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://[::1]:123/path"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "::1");
BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT(pUrl.path == "/path");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username == "user");
BEAST_EXPECT(pUrl.password == "pass");
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://user@domain:123/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username == "user");
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password == "pass");
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://domain:123/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username == "user");
BEAST_EXPECT(pUrl.password == "pass");
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://user@domain/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username == "user");
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://:pass@domain/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password == "pass");
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://domain/abc:321"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/abc:321");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme:///path/to/file"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain.empty());
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/path/to/file");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username == "user");
BEAST_EXPECT(pUrl.password == "pass");
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/path/with/an@sign");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://domain/path/with/an@sign"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "domain");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/path/with/an@sign");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "scheme://:999/"));
BEAST_EXPECT(pUrl.scheme == "scheme");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == ":999");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/");
}
{
ParsedUrl pUrl;
BEAST_EXPECT(parseUrl(pUrl, "http://::1:1234/validators"));
BEAST_EXPECT(pUrl.scheme == "http");
BEAST_EXPECT(pUrl.username.empty());
BEAST_EXPECT(pUrl.password.empty());
BEAST_EXPECT(pUrl.domain == "::0.1.18.52");
BEAST_EXPECT(!pUrl.port);
BEAST_EXPECT(pUrl.path == "/validators");
}
// Expected fails.
{
ParsedUrl pUrl;
BEAST_EXPECT(!parseUrl(pUrl, ""));
BEAST_EXPECT(!parseUrl(pUrl, "nonsense"));
BEAST_EXPECT(!parseUrl(pUrl, "://"));
BEAST_EXPECT(!parseUrl(pUrl, ":///"));
BEAST_EXPECT(!parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321"));
BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:23498765/"));
BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:0/"));
BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:+7/"));
BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:-7234/"));
BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:@#$56!/"));
}
{
std::string const strUrl("s://" + std::string(8192, ':'));
ParsedUrl pUrl;
BEAST_EXPECT(!parseUrl(pUrl, strUrl));
}
}
void
testToString()
{
testcase("toString");
auto result = to_string("hello");
BEAST_EXPECT(result == "hello");
}
void
run() override
{
testParseUrl();
testUnHex();
testToString();
}
};
BEAST_DEFINE_TESTSUITE(StringUtilities, basics, xrpl);
} // namespace xrpl

View File

@@ -1,251 +0,0 @@
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <memory>
#include <utility>
namespace xrpl {
/*
I guess you can put some items in, make sure they're still there. Let some
time pass, make sure they're gone. Keep a strong pointer to one of them, make
sure you can still find it even after time passes. Create two objects with
the same key, canonicalize them both and make sure you get the same object.
Put an object in but keep a strong pointer to it, advance the clock a lot,
then canonicalize a new object with the same key, make sure you get the
original object.
*/
class TaggedCache_test : public beast::unit_test::Suite
{
public:
void
run() override
{
using namespace std::chrono_literals;
using beast::Severity;
test::SuiteJournal journal("TaggedCache_test", *this);
TestStopwatch clock;
clock.set(0);
using Key = LedgerIndex;
using Value = std::string;
using Cache = TaggedCache<Key, Value>;
Cache c("test", 1, 1s, clock, journal);
// Insert an item, retrieve it, and age it so it gets purged.
{
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 0);
BEAST_EXPECT(!c.insert(1, "one"));
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.getTrackSize() == 1);
{
std::string s;
BEAST_EXPECT(c.retrieve(1, s));
BEAST_EXPECT(s == "one");
}
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 0);
}
// Insert an item, maintain a strong pointer, age it, and
// verify that the entry still exists.
{
BEAST_EXPECT(!c.insert(2, "two"));
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.getTrackSize() == 1);
{
auto p = c.fetch(2);
BEAST_EXPECT(p != nullptr);
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 1);
}
// Make sure its gone now that our reference is gone
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 0);
}
// Insert the same key/value pair and make sure we get the same result
{
BEAST_EXPECT(!c.insert(3, "three"));
{
auto const p1 = c.fetch(3);
auto p2 = std::make_shared<Value>("three");
c.canonicalizeReplaceClient(3, p2);
BEAST_EXPECT(p1.get() == p2.get());
}
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 0);
}
// Put an object in but keep a strong pointer to it, advance the clock a
// lot, then canonicalize a new object with the same key, make sure you
// get the original object.
{
// Put an object in
BEAST_EXPECT(!c.insert(4, "four"));
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.getTrackSize() == 1);
{
// Keep a strong pointer to it
auto const p1 = c.fetch(4);
BEAST_EXPECT(p1 != nullptr);
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.getTrackSize() == 1);
// Advance the clock a lot
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 1);
// Canonicalize a new object with the same key
auto p2 = std::make_shared<std::string>("four");
BEAST_EXPECT(c.canonicalizeReplaceClient(4, p2));
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.getTrackSize() == 1);
// Make sure we get the original object
BEAST_EXPECT(p1.get() == p2.get());
}
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.getTrackSize() == 0);
}
{
BEAST_EXPECT(!c.insert(5, "five"));
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.size() == 1);
{
auto const p1 = c.fetch(5);
BEAST_EXPECT(p1 != nullptr);
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.size() == 1);
// Advance the clock a lot
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.size() == 1);
auto p2 = std::make_shared<std::string>("five_2");
BEAST_EXPECT(c.canonicalizeReplaceCache(5, p2));
BEAST_EXPECT(c.getCacheSize() == 1);
BEAST_EXPECT(c.size() == 1);
// Make sure the caller's original pointer is unchanged
BEAST_EXPECT(p1.get() != p2.get());
BEAST_EXPECT(*p2 == "five_2");
auto const p3 = c.fetch(5);
BEAST_EXPECT(p3 != nullptr);
BEAST_EXPECT(p3.get() == p2.get());
BEAST_EXPECT(p3.get() != p1.get());
}
++clock;
c.sweep();
BEAST_EXPECT(c.getCacheSize() == 0);
BEAST_EXPECT(c.size() == 0);
}
{
testcase("intrptr");
struct MyRefCountObject : IntrusiveRefCounts
{
std::string data;
// Needed to support weak intrusive pointers
virtual void
partialDestructor() {};
MyRefCountObject() = default;
explicit MyRefCountObject(std::string data) : data(std::move(data))
{
}
bool
operator==(std::string const& other) const
{
return data == other;
}
};
using IntrPtrCache = TaggedCache<
Key,
MyRefCountObject,
/*IsKeyCache*/ false,
intr_ptr::SharedWeakUnionPtr<MyRefCountObject>,
intr_ptr::SharedPtr<MyRefCountObject>>;
IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal);
intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared<MyRefCountObject>("one"));
BEAST_EXPECT(intrPtrCache.getCacheSize() == 1);
BEAST_EXPECT(intrPtrCache.size() == 1);
{
{
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced"));
auto p = intrPtrCache.fetch(1);
BEAST_EXPECT(*p == "one_replaced");
// Advance the clock a lot
++clock;
intrPtrCache.sweep();
BEAST_EXPECT(intrPtrCache.getCacheSize() == 0);
BEAST_EXPECT(intrPtrCache.size() == 1);
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced_2"));
auto p2 = intrPtrCache.fetch(1);
BEAST_EXPECT(*p2 == "one_replaced_2");
intrPtrCache.del(1, true);
}
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced_3"));
auto p3 = intrPtrCache.fetch(1);
BEAST_EXPECT(*p3 == "one_replaced_3");
}
++clock;
intrPtrCache.sweep();
BEAST_EXPECT(intrPtrCache.getCacheSize() == 0);
BEAST_EXPECT(intrPtrCache.size() == 0);
}
}
};
BEAST_DEFINE_TESTSUITE(TaggedCache, basics, xrpl);
} // namespace xrpl

View File

@@ -1,344 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace xrpl::test {
class units_test : public beast::unit_test::Suite
{
private:
void
testTypes()
{
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
XRPAmount const x{100};
BEAST_EXPECT(x.drops() == 100);
BEAST_EXPECT((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
BEAST_EXPECT(y.value() == 400);
BEAST_EXPECT((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
auto z = 4 * y;
BEAST_EXPECT(z.value() == 1600);
BEAST_EXPECT((std::is_same_v<decltype(z)::unit_type, unit::dropTag>));
FeeLevel32 const f{10};
FeeLevel32 const baseFee{100};
auto drops = mulDiv(baseFee, x, f);
BEAST_EXPECT(drops);
BEAST_EXPECT(drops.value() == 1000); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT((std::is_same_v<
std::remove_reference_t<decltype(*drops)>::unit_type,
unit::dropTag>));
BEAST_EXPECT((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
XRPAmount const x{100};
BEAST_EXPECT(x.value() == 100);
BEAST_EXPECT((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
BEAST_EXPECT(y.value() == 400);
BEAST_EXPECT((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
FeeLevel64 const f{10};
FeeLevel64 const baseFee{100};
auto drops = mulDiv(baseFee, x, f);
BEAST_EXPECT(drops);
BEAST_EXPECT(drops.value() == 1000); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT((std::is_same_v<
std::remove_reference_t<decltype(*drops)>::unit_type,
unit::dropTag>));
BEAST_EXPECT((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
FeeLevel64 const x{1024};
BEAST_EXPECT(x.value() == 1024);
BEAST_EXPECT((std::is_same_v<decltype(x)::unit_type, unit::feelevelTag>));
std::uint64_t const m = 4;
auto y = m * x;
BEAST_EXPECT(y.value() == 4096);
BEAST_EXPECT((std::is_same_v<decltype(y)::unit_type, unit::feelevelTag>));
XRPAmount const basefee{10};
FeeLevel64 const referencefee{256};
auto drops = mulDiv(x, basefee, referencefee);
BEAST_EXPECT(drops);
BEAST_EXPECT(drops.value() == 40); // NOLINT(bugprone-unchecked-optional-access)
BEAST_EXPECT((std::is_same_v<
std::remove_reference_t<decltype(*drops)>::unit_type,
unit::dropTag>));
BEAST_EXPECT((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
}
void
testJson()
{
// Json value functionality
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::UInt);
BEAST_EXPECT(y == json::Value{x.fee()});
}
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::UInt);
BEAST_EXPECT(y == json::Value{x.fee()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::UInt);
BEAST_EXPECT(y == json::Value{std::numeric_limits<std::uint32_t>::max()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::UInt);
BEAST_EXPECT(y == json::Value{0});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Real);
BEAST_EXPECT(y == json::Value{std::numeric_limits<double>::max()});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Real);
BEAST_EXPECT(y == json::Value{std::numeric_limits<double>::min()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Int);
BEAST_EXPECT(y == json::Value{std::numeric_limits<std::int32_t>::max()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Int);
BEAST_EXPECT(y == json::Value{std::numeric_limits<std::int32_t>::min()});
}
}
void
testFunctions()
{
// Explicitly test every defined function for the ValueUnit class
// since some of them are templated, but not used anywhere else.
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
auto make = [&](auto x) -> FeeLevel64 { return x; };
auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; };
[[maybe_unused]]
FeeLevel64 const defaulted{};
FeeLevel64 test{0};
BEAST_EXPECT(test.fee() == 0);
test = explicitmake(beast::kZero);
BEAST_EXPECT(test.fee() == 0);
test = beast::kZero;
BEAST_EXPECT(test.fee() == 0);
test = explicitmake(100u);
BEAST_EXPECT(test.fee() == 100);
FeeLevel64 const targetSame{200u};
FeeLevel32 const targetOther{300u};
test = make(targetSame);
BEAST_EXPECT(test.fee() == 200);
BEAST_EXPECT(test == targetSame);
BEAST_EXPECT(test < FeeLevel64{1000});
BEAST_EXPECT(test > FeeLevel64{100});
test = make(targetOther);
BEAST_EXPECT(test.fee() == 300);
BEAST_EXPECT(test == targetOther);
test = std::uint64_t(200);
BEAST_EXPECT(test.fee() == 200);
test = std::uint32_t(300);
BEAST_EXPECT(test.fee() == 300);
test = targetSame;
BEAST_EXPECT(test.fee() == 200);
test = targetOther.fee();
BEAST_EXPECT(test.fee() == 300);
BEAST_EXPECT(test == targetOther);
test = targetSame * 2;
BEAST_EXPECT(test.fee() == 400);
test = 3 * targetSame;
BEAST_EXPECT(test.fee() == 600);
test = targetSame / 10;
BEAST_EXPECT(test.fee() == 20);
test += targetSame;
BEAST_EXPECT(test.fee() == 220);
test -= targetSame;
BEAST_EXPECT(test.fee() == 20);
test++;
BEAST_EXPECT(test.fee() == 21);
++test;
BEAST_EXPECT(test.fee() == 22);
test--;
BEAST_EXPECT(test.fee() == 21);
--test;
BEAST_EXPECT(test.fee() == 20);
test *= 5;
BEAST_EXPECT(test.fee() == 100);
test /= 2;
BEAST_EXPECT(test.fee() == 50);
test %= 13;
BEAST_EXPECT(test.fee() == 11);
/*
// illegal with unsigned
test = -test;
BEAST_EXPECT(test.fee() == -11);
BEAST_EXPECT(test.signum() == -1);
BEAST_EXPECT(to_string(test) == "-11");
*/
BEAST_EXPECT(test);
test = 0;
BEAST_EXPECT(!test);
BEAST_EXPECT(test.signum() == 0);
test = targetSame;
BEAST_EXPECT(test.signum() == 1);
BEAST_EXPECT(to_string(test) == "200");
}
{
auto make = [&](auto x) -> FeeLevelDouble { return x; };
auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; };
[[maybe_unused]]
FeeLevelDouble const defaulted{};
FeeLevelDouble test{0};
BEAST_EXPECT(test.fee() == 0);
test = explicitmake(beast::kZero);
BEAST_EXPECT(test.fee() == 0);
test = beast::kZero;
BEAST_EXPECT(test.fee() == 0);
test = explicitmake(100.0);
BEAST_EXPECT(test.fee() == 100);
FeeLevelDouble const targetSame{200.0};
FeeLevel64 const targetOther{300};
test = make(targetSame);
BEAST_EXPECT(test.fee() == 200);
BEAST_EXPECT(test == targetSame);
BEAST_EXPECT(test < FeeLevelDouble{1000.0});
BEAST_EXPECT(test > FeeLevelDouble{100.0});
test = targetOther.fee();
BEAST_EXPECT(test.fee() == 300);
BEAST_EXPECT(test == targetOther);
test = 200.0;
BEAST_EXPECT(test.fee() == 200);
test = std::uint64_t(300);
BEAST_EXPECT(test.fee() == 300);
test = targetSame;
BEAST_EXPECT(test.fee() == 200);
test = targetSame * 2;
BEAST_EXPECT(test.fee() == 400);
test = 3 * targetSame;
BEAST_EXPECT(test.fee() == 600);
test = targetSame / 10;
BEAST_EXPECT(test.fee() == 20);
test += targetSame;
BEAST_EXPECT(test.fee() == 220);
test -= targetSame;
BEAST_EXPECT(test.fee() == 20);
test++;
BEAST_EXPECT(test.fee() == 21);
++test;
BEAST_EXPECT(test.fee() == 22);
test--;
BEAST_EXPECT(test.fee() == 21);
--test;
BEAST_EXPECT(test.fee() == 20);
test *= 5;
BEAST_EXPECT(test.fee() == 100);
test /= 2;
BEAST_EXPECT(test.fee() == 50);
/* illegal with floating
test %= 13;
BEAST_EXPECT(test.fee() == 11);
*/
// legal with signed
test = -test;
BEAST_EXPECT(test.fee() == -50);
BEAST_EXPECT(test.signum() == -1);
BEAST_EXPECT(to_string(test) == "-50.000000");
BEAST_EXPECT(test);
test = 0;
BEAST_EXPECT(!test);
BEAST_EXPECT(test.signum() == 0);
test = targetSame;
BEAST_EXPECT(test.signum() == 1);
BEAST_EXPECT(to_string(test) == "200.000000");
}
}
public:
void
run() override
{
BEAST_EXPECT(kInitialXrp.drops() == 100'000'000'000'000'000);
BEAST_EXPECT(kInitialXrp == XRPAmount{100'000'000'000'000'000});
testTypes();
testJson();
testFunctions();
}
};
BEAST_DEFINE_TESTSUITE(units, basics, xrpl);
} // namespace xrpl::test

View File

@@ -1,326 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <limits>
namespace xrpl {
class XRPAmount_test : public beast::unit_test::Suite
{
public:
void
testSigNum()
{
testcase("signum");
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
if (i < 0)
{
BEAST_EXPECT(x.signum() < 0);
}
else if (i > 0)
{
BEAST_EXPECT(x.signum() > 0);
}
else
{
BEAST_EXPECT(x.signum() == 0);
}
}
}
void
testBeastZero()
{
testcase("beast::Zero Comparisons");
using beast::kZero;
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
BEAST_EXPECT((i == 0) == (x == kZero));
BEAST_EXPECT((i != 0) == (x != kZero));
BEAST_EXPECT((i < 0) == (x < kZero));
BEAST_EXPECT((i > 0) == (x > kZero));
BEAST_EXPECT((i <= 0) == (x <= kZero));
BEAST_EXPECT((i >= 0) == (x >= kZero));
BEAST_EXPECT((0 == i) == (kZero == x));
BEAST_EXPECT((0 != i) == (kZero != x));
BEAST_EXPECT((0 < i) == (kZero < x));
BEAST_EXPECT((0 > i) == (kZero > x));
BEAST_EXPECT((0 <= i) == (kZero <= x));
BEAST_EXPECT((0 >= i) == (kZero >= x));
}
}
void
testComparisons()
{
testcase("XRP Comparisons");
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
for (auto j : {-1, 0, 1})
{
XRPAmount const y(j);
BEAST_EXPECT((i == j) == (x == y));
BEAST_EXPECT((i != j) == (x != y));
BEAST_EXPECT((i < j) == (x < y));
BEAST_EXPECT((i > j) == (x > y));
BEAST_EXPECT((i <= j) == (x <= y));
BEAST_EXPECT((i >= j) == (x >= y));
}
}
}
void
testAddSub()
{
testcase("Addition & Subtraction");
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
for (auto j : {-1, 0, 1})
{
XRPAmount const y(j);
BEAST_EXPECT(XRPAmount(i + j) == (x + y));
BEAST_EXPECT(XRPAmount(i - j) == (x - y));
BEAST_EXPECT((x + y) == (y + x)); // addition is commutative
}
}
}
void
testDecimal()
{
// Tautology
BEAST_EXPECT(kDropsPerXrp.decimalXRP() == 1);
XRPAmount test{1};
BEAST_EXPECT(test.decimalXRP() == 0.000001);
test = -test;
BEAST_EXPECT(test.decimalXRP() == -0.000001);
test = 100'000'000;
BEAST_EXPECT(test.decimalXRP() == 100);
test = -test;
BEAST_EXPECT(test.decimalXRP() == -100);
}
void
testFunctions()
{
// Explicitly test every defined function for the XRPAmount class
// since some of them are templated, but not used anywhere else.
auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; };
XRPAmount const defaulted{};
(void)defaulted;
XRPAmount test{0};
BEAST_EXPECT(test.drops() == 0);
test = make(beast::kZero);
BEAST_EXPECT(test.drops() == 0);
test = beast::kZero;
BEAST_EXPECT(test.drops() == 0);
test = make(100);
BEAST_EXPECT(test.drops() == 100);
test = make(100u);
BEAST_EXPECT(test.drops() == 100);
XRPAmount const targetSame{200u};
test = make(targetSame);
BEAST_EXPECT(test.drops() == 200);
BEAST_EXPECT(test == targetSame);
BEAST_EXPECT(test < XRPAmount{1000});
BEAST_EXPECT(test > XRPAmount{100});
test = std::int64_t(200);
BEAST_EXPECT(test.drops() == 200);
test = std::uint32_t(300);
BEAST_EXPECT(test.drops() == 300);
test = targetSame;
BEAST_EXPECT(test.drops() == 200);
auto testOther = test.dropsAs<std::uint32_t>();
BEAST_EXPECT(testOther);
BEAST_EXPECT(*testOther == 200); // NOLINT(bugprone-unchecked-optional-access)
test = std::numeric_limits<std::uint64_t>::max();
testOther = test.dropsAs<std::uint32_t>();
BEAST_EXPECT(!testOther);
test = -1;
testOther = test.dropsAs<std::uint32_t>();
BEAST_EXPECT(!testOther);
test = targetSame * 2;
BEAST_EXPECT(test.drops() == 400);
test = 3 * targetSame;
BEAST_EXPECT(test.drops() == 600);
test = 20;
BEAST_EXPECT(test.drops() == 20);
test += targetSame;
BEAST_EXPECT(test.drops() == 220);
test -= targetSame;
BEAST_EXPECT(test.drops() == 20);
test *= 5;
BEAST_EXPECT(test.drops() == 100);
test = 50;
BEAST_EXPECT(test.drops() == 50);
test -= 39;
BEAST_EXPECT(test.drops() == 11);
// legal with signed
test = -test;
BEAST_EXPECT(test.drops() == -11);
BEAST_EXPECT(test.signum() == -1);
BEAST_EXPECT(to_string(test) == "-11");
BEAST_EXPECT(test);
test = 0;
BEAST_EXPECT(!test);
BEAST_EXPECT(test.signum() == 0);
test = targetSame;
BEAST_EXPECT(test.signum() == 1);
BEAST_EXPECT(to_string(test) == "200");
}
void
testMulRatio()
{
testcase("mulRatio");
constexpr auto kMaxUInt32 = std::numeric_limits<std::uint32_t>::max();
constexpr auto kMaxXrp = std::numeric_limits<XRPAmount::value_type>::max();
constexpr auto kMinXrp = std::numeric_limits<XRPAmount::value_type>::min();
{
// multiply by a number that would overflow then divide by the same
// number, and check we didn't lose any value
XRPAmount big(kMaxXrp);
BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, true));
// rounding mode shouldn't matter as the result is exact
BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, false));
// multiply and divide by values that would overflow if done
// naively, and check that it gives the correct answer
big -= 0xf; // Subtract a little so it's divisible by 4
BEAST_EXPECT(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3);
BEAST_EXPECT(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3);
BEAST_EXPECT((big.value() * 3) / 4 != (big.value() / 4) * 3);
}
{
// Similar test as above, but for negative values
XRPAmount big(kMinXrp); // NOLINT TODO
BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, true));
// rounding mode shouldn't matter as the result is exact
BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, false));
// multiply and divide by values that would overflow if done
// naively, and check that it gives the correct answer
BEAST_EXPECT(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3);
BEAST_EXPECT(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3);
BEAST_EXPECT((big.value() * 3) / 4 != (big.value() / 4) * 3);
}
{
// small amounts
XRPAmount const tiny(1);
// Round up should give the smallest allowable number
BEAST_EXPECT(tiny == mulRatio(tiny, 1, kMaxUInt32, true));
// rounding down should be zero
BEAST_EXPECT(beast::kZero == mulRatio(tiny, 1, kMaxUInt32, false));
BEAST_EXPECT(beast::kZero == mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false));
// tiny negative numbers
XRPAmount const tinyNeg(-1);
// Round up should give zero
BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, 1, kMaxUInt32, true));
BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true));
// rounding down should be tiny
BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false));
}
{ // rounding
{
XRPAmount const one(1);
auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false);
BEAST_EXPECT(rup.drops() - rdown.drops() == 1);
}
{
XRPAmount const big(kMaxXrp);
auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false);
BEAST_EXPECT(rup.drops() - rdown.drops() == 1);
}
{
XRPAmount const negOne(-1);
auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false);
BEAST_EXPECT(rup.drops() - rdown.drops() == 1);
}
}
{
// division by zero
XRPAmount one(1);
except([&] { mulRatio(one, 1, 0, true); });
}
{
// overflow
XRPAmount big(kMaxXrp);
except([&] { mulRatio(big, 2, 1, true); });
}
{
// underflow
XRPAmount const bigNegative(kMinXrp + 10);
BEAST_EXPECT(mulRatio(bigNegative, 2, 1, true) == kMinXrp);
}
} // namespace xrpl
//--------------------------------------------------------------------------
void
run() override
{
testSigNum();
testBeastZero();
testComparisons();
testAddSub();
testDecimal();
testFunctions();
testMulRatio();
}
};
BEAST_DEFINE_TESTSUITE(XRPAmount, basics, xrpl);
} // namespace xrpl

View File

@@ -1,440 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/detail/token_errors.h>
#include <boost/multiprecision/cpp_int.hpp> // IWYU pragma: keep
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <ranges>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
#ifndef _MSC_VER
#include <xrpl/protocol/detail/b58_utils.h>
#include <xrpl/protocol/tokens.h>
#include <array>
#include <cstddef>
#include <random>
#include <span>
#include <sstream>
namespace xrpl::test {
namespace {
[[nodiscard]] inline auto
randEngine() -> std::mt19937&
{
static std::mt19937 kR = [] {
std::random_device rd;
return std::mt19937{rd()};
}();
return kR;
}
constexpr int kNumTokenTypeIndexes = 9;
[[nodiscard]] inline auto
tokenTypeAndSize(int i) -> std::tuple<xrpl::TokenType, std::size_t>
{
assert(i < kNumTokenTypeIndexes);
switch (i)
{
using enum xrpl::TokenType;
case 0:
return {None, 20};
case 1:
return {NodePublic, 32};
case 2:
return {NodePublic, 33};
case 3:
return {NodePrivate, 32};
case 4:
return {AccountID, 20};
case 5:
return {AccountPublic, 32};
case 6:
return {AccountPublic, 33};
case 7:
return {AccountSecret, 32};
case 8:
return {FamilySeed, 16};
default:
throw std::invalid_argument(
"Invalid token selection passed to tokenTypeAndSize() "
"in " __FILE__);
}
}
[[nodiscard]] inline auto
randomTokenTypeAndSize() -> std::tuple<xrpl::TokenType, std::size_t>
{
using namespace xrpl;
auto& rng = randEngine();
std::uniform_int_distribution<> d(0, 8);
return tokenTypeAndSize(d(rng));
}
// Return the token type and subspan of `d` to use as test data.
[[nodiscard]] inline auto
randomB256TestData(std::span<std::uint8_t> d)
-> std::tuple<xrpl::TokenType, std::span<std::uint8_t>>
{
auto& rng = randEngine();
std::uniform_int_distribution<std::uint8_t> dist(0, 255);
auto [tokType, tokSize] = randomTokenTypeAndSize();
std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); });
return {tokType, d.subspan(0, tokSize)};
}
inline void
printAsChar(std::span<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> s) {
std::string r;
r.resize(s.size());
std::ranges::copy(s, r.begin());
return r;
};
auto sa = asString(a);
auto sb = asString(b);
std::cerr << "\n\n" << sa << "\n" << sb << "\n";
}
inline void
printAsInt(std::span<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> s) -> std::string {
std::stringstream sstr;
for (auto i : s)
{
sstr << std::setw(3) << int(i) << ',';
}
return sstr.str();
};
auto sa = asString(a);
auto sb = asString(b);
std::cerr << "\n\n" << sa << "\n" << sb << "\n";
}
} // namespace
namespace multiprecision_utils {
boost::multiprecision::checked_uint512_t
toBoostMP(std::span<std::uint64_t> in)
{
boost::multiprecision::checked_uint512_t mbp = 0;
for (auto& word : std::views::reverse(in))
{
mbp <<= 64;
mbp += word;
}
return mbp;
}
std::vector<std::uint64_t>
randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5)
{
auto eng = randEngine();
std::uniform_int_distribution<std::uint8_t> numCoeffDist(minSize, maxSize);
std::uniform_int_distribution<std::uint64_t> dist;
auto const numCoeff = numCoeffDist(eng);
std::vector<std::uint64_t> coeffs;
coeffs.reserve(numCoeff);
for (int i = 0; i < numCoeff; ++i)
{
coeffs.push_back(dist(eng));
}
return coeffs;
}
} // namespace multiprecision_utils
class base58_test : public beast::unit_test::Suite
{
void
testMultiprecision()
{
testcase("b58_multiprecision");
using namespace boost::multiprecision;
static constexpr std::size_t kIters = 100000;
auto eng = randEngine();
std::uniform_int_distribution<std::uint64_t> dist;
std::uniform_int_distribution<std::uint64_t> dist1(1);
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
if (d == 0u)
continue;
auto bigInt = multiprecision_utils::randomBigInt();
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refDiv = boostBigInt / d;
auto const refMod = boostBigInt % d;
auto const mod = b58_fast::detail::inplaceBigintDivRem(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
auto const foundDiv = multiprecision_utils::toBoostMP(bigInt);
BEAST_EXPECT(refMod.convert_to<std::uint64_t>() == mod);
BEAST_EXPECT(foundDiv == refDiv);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2);
if (bigInt[bigInt.size() - 1] == std::numeric_limits<std::uint64_t>::max())
{
bigInt[bigInt.size() - 1] -= 1; // Prevent overflow
}
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
BEAST_EXPECT(result == TokenCodecErrc::Success);
auto const foundAdd = multiprecision_utils::toBoostMP(bigInt);
BEAST_EXPECT(refAdd == foundAdd);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist1(eng);
// Force overflow
std::vector<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
BEAST_EXPECT(result == TokenCodecErrc::OverflowAdd);
auto const foundAdd = multiprecision_utils::toBoostMP(bigInt);
BEAST_EXPECT(refAdd != foundAdd);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2);
// inplace mul requires the most significant coeff to be zero to
// hold the result.
bigInt[bigInt.size() - 1] = 0;
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
BEAST_EXPECT(result == TokenCodecErrc::Success);
auto const foundMul = multiprecision_utils::toBoostMP(bigInt);
BEAST_EXPECT(refMul == foundMul);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist1(eng);
// Force overflow
std::vector<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
BEAST_EXPECT(result == TokenCodecErrc::InputTooLarge);
auto const foundMul = multiprecision_utils::toBoostMP(bigInt);
BEAST_EXPECT(refMul != foundMul);
}
}
void
testFastMatchesRef()
{
testcase("fast_matches_ref");
auto testRawEncode = [&](std::span<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b256Result;
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b58ResultBuf[i]};
if (i == 0)
{
auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf);
BEAST_EXPECT(r);
b58Result[i] = r.value();
}
else
{
std::array<std::uint8_t, 128> tmpBuf{};
std::string const s = xrpl::b58_ref::detail::encodeBase58(
b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size());
BEAST_EXPECT(s.size());
b58Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b58Result[i].begin());
}
}
if (BEAST_EXPECT(b58Result[0].size() == b58Result[1].size()))
{
if (!BEAST_EXPECT(
memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0))
{
printAsChar(b58Result[0], b58Result[1]);
}
}
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()};
if (i == 0)
{
std::string const in(
b58Result[i].data(), b58Result[i].data() + b58Result[i].size());
auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf);
BEAST_EXPECT(r);
b256Result[i] = r.value();
}
else
{
std::string const st(b58Result[i].begin(), b58Result[i].end());
std::string const s = xrpl::b58_ref::detail::decodeBase58(st);
BEAST_EXPECT(s.size());
b256Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b256Result[i].begin());
}
}
if (BEAST_EXPECT(b256Result[0].size() == b256Result[1].size()))
{
if (!BEAST_EXPECT(
memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) ==
0))
{
printAsInt(b256Result[0], b256Result[1]);
}
}
};
auto testTokenEncode = [&](xrpl::TokenType const tokType,
std::span<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b256Result;
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()};
if (i == 0)
{
auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf);
BEAST_EXPECT(r);
b58Result[i] = r.value();
}
else
{
std::string const s =
xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size());
BEAST_EXPECT(s.size());
b58Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b58Result[i].begin());
}
}
if (BEAST_EXPECT(b58Result[0].size() == b58Result[1].size()))
{
if (!BEAST_EXPECT(
memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0))
{
printAsChar(b58Result[0], b58Result[1]);
}
}
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()};
if (i == 0)
{
std::string const in(
b58Result[i].data(), b58Result[i].data() + b58Result[i].size());
auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf);
BEAST_EXPECT(r);
b256Result[i] = r.value();
}
else
{
std::string const st(b58Result[i].begin(), b58Result[i].end());
std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType);
BEAST_EXPECT(s.size());
b256Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b256Result[i].begin());
}
}
if (BEAST_EXPECT(b256Result[0].size() == b256Result[1].size()))
{
if (!BEAST_EXPECT(
memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) ==
0))
{
printAsInt(b256Result[0], b256Result[1]);
}
}
};
auto testIt = [&](xrpl::TokenType const tokType, std::span<std::uint8_t> const& b256Data) {
testRawEncode(b256Data);
testTokenEncode(tokType, b256Data);
};
// test every token type with data where every byte is the same and the
// bytes range from 0-255
for (int i = 0; i < kNumTokenTypeIndexes; ++i)
{
std::array<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, tokSize] = tokenTypeAndSize(i);
for (int d = 0; d <= 255; ++d)
{
memset(b256DataBuf.data(), d, tokSize);
testIt(tokType, std::span(b256DataBuf.data(), tokSize));
}
}
// test with random data
static constexpr std::size_t kIters = 100000;
for (int i = 0; i < kIters; ++i)
{
std::array<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, b256Data] = randomB256TestData(b256DataBuf);
testIt(tokType, b256Data);
}
}
void
run() override
{
testMultiprecision();
testFastMatchesRef();
}
};
BEAST_DEFINE_TESTSUITE(base58, basics, xrpl);
} // namespace xrpl::test
#endif // _MSC_VER

View File

@@ -1,376 +0,0 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <boost/endian/detail/order.hpp>
#include <array>
#include <cassert>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl::test {
// a non-hashing Hasher that just copies the bytes.
// Used to test hash_append in base_uint
template <std::size_t Bits>
struct Nonhash
{
static constexpr auto kEndian = boost::endian::order::big;
static constexpr std::size_t kWidth = Bits / 8;
std::array<std::uint8_t, kWidth> data;
Nonhash() = default;
void
operator()(void const* key, std::size_t len) noexcept
{
assert(len == kWidth);
memcpy(data.data(), key, len);
}
explicit
operator std::size_t() noexcept
{
return kWidth;
}
};
struct base_uint_test : beast::unit_test::Suite
{
using test96 = BaseUInt<96>;
static_assert(std::is_copy_constructible_v<test96>);
static_assert(std::is_copy_assignable_v<test96>);
void
testComparisons()
{
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 6> kTestArgs{
{{"0000000000000000", "0000000000000001"},
{"0000000000000000", "ffffffffffffffff"},
{"1234567812345678", "2345678923456789"},
{"8000000000000000", "8000000000000001"},
{"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"},
{"fffffffffffffffe", "ffffffffffffffff"}}};
for (auto const& arg : kTestArgs)
{
xrpl::BaseUInt<64> const u{arg.first}, v{arg.second};
BEAST_EXPECT(u < v);
BEAST_EXPECT(u <= v);
BEAST_EXPECT(u != v);
BEAST_EXPECT(!(u == v));
BEAST_EXPECT(!(u > v));
BEAST_EXPECT(!(u >= v));
BEAST_EXPECT(!(v < u));
BEAST_EXPECT(!(v <= u));
BEAST_EXPECT(v != u);
BEAST_EXPECT(!(v == u));
BEAST_EXPECT(v > u);
BEAST_EXPECT(v >= u);
BEAST_EXPECT(u == u);
BEAST_EXPECT(v == v);
}
}
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 6> kTestArgs{
{
{"000000000000000000000000", "000000000000000000000001"},
{"000000000000000000000000", "ffffffffffffffffffffffff"},
{"0123456789ab0123456789ab", "123456789abc123456789abc"},
{"555555555555555555555555", "55555555555a555555555555"},
{"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"},
{"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"},
}};
for (auto const& arg : kTestArgs)
{
xrpl::BaseUInt<96> const u{arg.first}, v{arg.second};
BEAST_EXPECT(u < v);
BEAST_EXPECT(u <= v);
BEAST_EXPECT(u != v);
BEAST_EXPECT(!(u == v));
BEAST_EXPECT(!(u > v));
BEAST_EXPECT(!(u >= v));
BEAST_EXPECT(!(v < u));
BEAST_EXPECT(!(v <= u));
BEAST_EXPECT(v != u);
BEAST_EXPECT(!(v == u));
BEAST_EXPECT(v > u);
BEAST_EXPECT(v >= u);
BEAST_EXPECT(u == u);
BEAST_EXPECT(v == v);
}
}
}
void
testFromRawSizeMismatch()
{
testcase("base_uint: fromRaw size mismatch");
// Container larger than the base_uint (16 bytes vs 12 bytes for test96).
// Only the first 12 bytes are copied; the extra bytes are ignored.
{
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
test96 const result = test96::fromRaw(tooBig);
BEAST_EXPECT(to_string(result) == "0102030405060708090A0B0C");
}
}
void
run() override
{
testcase("base_uint: general purpose tests");
#ifdef NDEBUG
testFromRawSizeMismatch();
#endif
static_assert(!std::is_constructible_v<test96, std::complex<double>>);
static_assert(!std::is_assignable_v<test96&, std::complex<double>>);
testComparisons();
// used to verify set insertion (hashing required)
std::unordered_set<test96, HardenedHash<>> uset;
Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
BEAST_EXPECT(test96::kBytes == raw.size());
test96 u = test96::fromRaw(raw);
uset.insert(u);
BEAST_EXPECT(raw.size() == u.size());
BEAST_EXPECT(to_string(u) == "0102030405060708090A0B0C");
BEAST_EXPECT(toShortString(u) == "01020304...");
BEAST_EXPECT(*u.data() == 1);
BEAST_EXPECT(u.signum() == 1);
BEAST_EXPECT(!!u);
BEAST_EXPECT(!u.isZero());
BEAST_EXPECT(u.isNonZero());
unsigned char t = 0;
for (auto& d : u)
{
BEAST_EXPECT(d == ++t);
}
// Test hash_append by "hashing" with a no-op hasher (h)
// and then extracting the bytes that were written during hashing
// back into another base_uint (w) for comparison with the original
Nonhash<96> h{};
hash_append(h, u);
test96 const w = test96::fromRaw(std::vector<std::uint8_t>(h.data.begin(), h.data.end()));
BEAST_EXPECT(w == u);
test96 v{~u};
uset.insert(v);
BEAST_EXPECT(to_string(v) == "FEFDFCFBFAF9F8F7F6F5F4F3");
BEAST_EXPECT(toShortString(v) == "FEFDFCFB...");
BEAST_EXPECT(*v.data() == 0xfe);
BEAST_EXPECT(v.signum() == 1);
BEAST_EXPECT(!!v);
BEAST_EXPECT(!v.isZero());
BEAST_EXPECT(v.isNonZero());
t = 0xff;
for (auto& d : v)
{
BEAST_EXPECT(d == --t);
}
BEAST_EXPECT(u < v);
BEAST_EXPECT(v > u);
v = u;
BEAST_EXPECT(v == u);
test96 z{beast::kZero};
uset.insert(z);
BEAST_EXPECT(to_string(z) == "000000000000000000000000");
BEAST_EXPECT(toShortString(z) == "00000000...");
BEAST_EXPECT(*z.data() == 0);
BEAST_EXPECT(*z.begin() == 0);
BEAST_EXPECT(*std::prev(z.end(), 1) == 0);
BEAST_EXPECT(z.signum() == 0);
BEAST_EXPECT(!z);
BEAST_EXPECT(z.isZero());
BEAST_EXPECT(!z.isNonZero());
for (auto& d : z)
{
BEAST_EXPECT(d == 0);
}
test96 n{z};
n++;
BEAST_EXPECT(n == test96(1));
n--;
BEAST_EXPECT(n == beast::kZero);
BEAST_EXPECT(n == z);
n--;
BEAST_EXPECT(to_string(n) == "FFFFFFFFFFFFFFFFFFFFFFFF");
BEAST_EXPECT(toShortString(n) == "FFFFFFFF...");
n = beast::kZero;
BEAST_EXPECT(n == z);
test96 zp1{z};
zp1++;
test96 zm1{z};
zm1--;
test96 const x{zm1 ^ zp1};
uset.insert(x);
BEAST_EXPECTS(to_string(x) == "FFFFFFFFFFFFFFFFFFFFFFFE", to_string(x));
BEAST_EXPECTS(toShortString(x) == "FFFFFFFF...", toShortString(x));
BEAST_EXPECT(uset.size() == 4);
test96 tmp;
BEAST_EXPECT(tmp.parseHex(to_string(u)));
BEAST_EXPECT(tmp == u);
tmp = z;
// fails with extra char
BEAST_EXPECT(!tmp.parseHex("A" + to_string(u)));
tmp = z;
// fails with extra char at end
BEAST_EXPECT(!tmp.parseHex(to_string(u) + "A"));
// fails with a non-hex character at some point in the string:
tmp = z;
for (std::size_t i = 0; i != 24; ++i)
{
std::string x = to_string(z);
x[i] = ('G' + (i % 10));
BEAST_EXPECT(!tmp.parseHex(x));
}
// Walking 1s:
for (std::size_t i = 0; i != 24; ++i)
{
std::string s1 = "000000000000000000000000";
s1[i] = '1';
BEAST_EXPECT(tmp.parseHex(s1));
BEAST_EXPECT(to_string(tmp) == s1);
}
// Walking 0s:
for (std::size_t i = 0; i != 24; ++i)
{
std::string s1 = "111111111111111111111111";
s1[i] = '0';
BEAST_EXPECT(tmp.parseHex(s1));
BEAST_EXPECT(to_string(tmp) == s1);
}
// Constexpr constructors
{
static_assert(test96{}.signum() == 0);
static_assert(test96("0").signum() == 0);
static_assert(test96("000000000000000000000000").signum() == 0);
static_assert(test96("000000000000000000000001").signum() == 1);
static_assert(test96("800000000000000000000000").signum() == 1);
// Everything within the #if should fail during compilation.
#if 0
// Too few characters
static_assert(test96("00000000000000000000000").signum() == 0);
// Too many characters
static_assert(test96("0000000000000000000000000").signum() == 0);
// Non-hex characters
static_assert(test96("00000000000000000000000 ").signum() == 1);
static_assert(test96("00000000000000000000000/").signum() == 1);
static_assert(test96("00000000000000000000000:").signum() == 1);
static_assert(test96("00000000000000000000000@").signum() == 1);
static_assert(test96("00000000000000000000000G").signum() == 1);
static_assert(test96("00000000000000000000000`").signum() == 1);
static_assert(test96("00000000000000000000000g").signum() == 1);
static_assert(test96("00000000000000000000000~").signum() == 1);
#endif // 0
// Using the constexpr constructor in a non-constexpr context
// with an error in the parsing throws an exception.
{
// Invalid length for string.
bool caught = false;
try
{
// Try to prevent constant evaluation.
std::vector<char> str(23, '7');
std::string_view const sView(str.data(), str.size());
[[maybe_unused]] test96 const t96(sView);
}
catch (std::invalid_argument const& e)
{
BEAST_EXPECT(e.what() == std::string("invalid length for hex string"));
caught = true;
}
BEAST_EXPECT(caught);
}
{
// Invalid character in string.
bool caught = false;
try
{
// Try to prevent constant evaluation.
std::vector<char> str(23, '7');
str.push_back('G');
std::string_view const sView(str.data(), str.size());
[[maybe_unused]] test96 const t96(sView);
}
catch (std::range_error const& e)
{
BEAST_EXPECT(e.what() == std::string("invalid hex character"));
caught = true;
}
BEAST_EXPECT(caught);
}
// Verify that constexpr base_uints interpret a string the same
// way parseHex() does.
struct StrBaseUInt
{
char const* const str;
test96 tst;
constexpr StrBaseUInt(char const* s) : str(s), tst(s)
{
}
};
static constexpr StrBaseUInt kTestCases[] = {
"000000000000000000000000",
"000000000000000000000001",
"fedcba9876543210ABCDEF91",
"19FEDCBA0123456789abcdef",
"800000000000000000000000",
"fFfFfFfFfFfFfFfFfFfFfFfF"};
for (StrBaseUInt const& t : kTestCases)
{
test96 t96;
BEAST_EXPECT(t96.parseHex(t.str));
BEAST_EXPECT(t96 == t.tst);
}
}
}
};
BEAST_DEFINE_TESTSUITE(base_uint, basics, xrpl);
} // namespace xrpl::test

View File

@@ -1,83 +0,0 @@
#include <test/jtx/Account.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/join.h>
#include <xrpl/beast/unit_test/suite.h>
#include <array>
#include <cstddef>
#include <initializer_list>
#include <sstream>
#include <string>
#include <vector>
namespace xrpl::test {
struct join_test : beast::unit_test::Suite
{
void
run() override
{
auto test = [this](auto collectionanddelimiter, std::string expected) {
std::stringstream ss;
// Put something else in the buffer before and after to ensure that
// the << operator returns the stream correctly.
ss << "(" << collectionanddelimiter << ")";
auto const str = ss.str();
BEAST_EXPECT(str.substr(1, str.length() - 2) == expected);
BEAST_EXPECT(str.front() == '(');
BEAST_EXPECT(str.back() == ')');
};
// C++ array
test(CollectionAndDelimiter(std::array<int, 4>{2, -1, 5, 10}, "/"), "2/-1/5/10");
// One item C++ array edge case
test(CollectionAndDelimiter(std::array<std::string, 1>{"test"}, " & "), "test");
// Empty C++ array edge case
test(CollectionAndDelimiter(std::array<int, 0>{}, ","), "");
{
// C-style array
char letters[4]{'w', 'a', 's', 'd'};
test(CollectionAndDelimiter(letters, std::to_string(0)), "w0a0s0d");
}
{
// Auto sized C-style array
std::string words[]{"one", "two", "three", "four"};
test(CollectionAndDelimiter(words, "\n"), "one\ntwo\nthree\nfour");
}
{
// One item C-style array edge case
std::string words[]{"thing"};
test(CollectionAndDelimiter(words, "\n"), "thing");
}
// Initializer list
test(CollectionAndDelimiter(std::initializer_list<size_t>{19, 25}, "+"), "19+25");
// vector
test(CollectionAndDelimiter(std::vector<int>{0, 42}, std::to_string(99)), "09942");
{
// vector with one item edge case
using namespace jtx;
test(
CollectionAndDelimiter(std::vector<Account>{Account::kMaster}, "xxx"),
Account::kMaster.human());
}
// empty vector edge case
test(CollectionAndDelimiter(std::vector<uint256>{}, ","), "");
// C-style string
test(CollectionAndDelimiter("string", " "), "s t r i n g");
// Empty C-style string edge case
test(CollectionAndDelimiter("", "*"), "");
// Single char C-style string edge case
test(CollectionAndDelimiter("x", "*"), "x");
// std::string
test(CollectionAndDelimiter(std::string{"string"}, "-"), "s-t-r-i-n-g");
// Empty std::string edge case
test(CollectionAndDelimiter(std::string{""}, "*"), "");
// Single char std::string edge case
test(CollectionAndDelimiter(std::string{"y"}, "*"), "y");
}
}; // namespace test
BEAST_DEFINE_TESTSUITE(join, basics, xrpl);
} // namespace xrpl::test

View File

@@ -63,8 +63,8 @@ public:
negotiateProtocolVersion("RTXP/1.2, XRPL/2.0, XRPL/2.1") == makeProtocol(2, 1));
BEAST_EXPECT(negotiateProtocolVersion("XRPL/2.2") == makeProtocol(2, 2));
BEAST_EXPECT(
negotiateProtocolVersion("RTXP/1.2, XRPL/2.2, XRPL/2.3, XRPL/999.999") ==
makeProtocol(2, 2));
negotiateProtocolVersion("RTXP/1.2, XRPL/2.3, XRPL/2.4, XRPL/999.999") ==
makeProtocol(2, 3));
BEAST_EXPECT(negotiateProtocolVersion("XRPL/999.999, WebSocket/1.0") == std::nullopt);
BEAST_EXPECT(negotiateProtocolVersion("") == std::nullopt);
}

View File

@@ -1,296 +0,0 @@
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/net/IPAddressV4.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/resource/Disposition.h>
#include <xrpl/resource/Gossip.h>
#include <xrpl/resource/detail/Logic.h>
#include <xrpl/resource/detail/Tuning.h>
#include <boost/utility/base_from_member.hpp>
#include <chrono>
#include <cstdint>
#include <functional>
#include <string>
namespace xrpl::Resource {
class ResourceManager_test : public beast::unit_test::Suite
{
public:
class TestLogic : private boost::base_from_member<TestStopwatch>, public Logic
{
private:
using clock_type = boost::base_from_member<TestStopwatch>;
public:
explicit TestLogic(beast::Journal journal)
: Logic(beast::insight::NullCollector::make(), member, journal)
{
}
void
advance()
{
++member;
}
TestStopwatch&
clock()
{
return member;
}
};
//--------------------------------------------------------------------------
static void
createGossip(Gossip& gossip)
{
std::uint8_t const v(10 + randInt(9));
std::uint8_t const n(10 + randInt(9));
gossip.items.reserve(n);
for (std::uint8_t i = 0; i < n; ++i)
{
Gossip::Item item;
item.balance = 100 + randInt(499);
beast::IP::AddressV4::bytes_type const d = {
{192, 0, 2, static_cast<std::uint8_t>(v + i)}};
item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}};
gossip.items.push_back(item);
}
}
//--------------------------------------------------------------------------
void
testDrop(beast::Journal j, bool limited)
{
if (limited)
{
testcase("Limited warn/drop");
}
else
{
testcase("Unlimited warn/drop");
}
TestLogic logic(j);
Charge const fee(kDropThreshold + 1);
beast::IP::Endpoint const addr(beast::IP::Endpoint::fromString("192.0.2.2"));
std::function<Consumer(beast::IP::Endpoint)> const ep =
[&logic, limited](beast::IP::Endpoint const& address) {
return limited ? logic.newInboundEndpoint(address)
: logic.newUnlimitedEndpoint(address);
};
{
Consumer c(ep(addr));
// Create load until we get a warning
int n = 10000;
while (--n >= 0)
{
if (n == 0)
{
if (limited)
{
fail("Loop count exceeded without warning");
}
else
{
pass();
}
return;
}
if (c.charge(fee) == Disposition::Warn)
{
if (limited)
{
pass();
}
else
{
fail("Should loop forever with no warning");
}
break;
}
++logic.clock();
}
// Create load until we get dropped
while (--n >= 0)
{
if (n == 0)
{
if (limited)
{
fail("Loop count exceeded without dropping");
}
else
{
pass();
}
return;
}
if (c.charge(fee) == Disposition::Drop)
{
// Disconnect abusive Consumer
BEAST_EXPECT(c.disconnect(j) == limited);
break;
}
++logic.clock();
}
}
// Make sure the consumer is on the blacklist for a while.
{
Consumer const c(logic.newInboundEndpoint(addr));
logic.periodicActivity();
if (c.disposition() != Disposition::Drop)
{
if (limited)
{
fail("Dropped consumer not put on blacklist");
}
else
{
pass();
}
return;
}
}
// Makes sure the Consumer is eventually removed from blacklist
bool readmitted = false;
{
using namespace std::chrono_literals;
// Give Consumer time to become readmitted. Should never
// exceed expiration time.
auto n = kSecondsUntilExpiration + 1s;
while (--n > 0s)
{
++logic.clock();
logic.periodicActivity();
Consumer const c(logic.newInboundEndpoint(addr));
if (c.disposition() != Disposition::Drop)
{
readmitted = true;
break;
}
}
}
if (!readmitted)
{
fail("Dropped Consumer left on blacklist too long");
return;
}
pass();
}
void
testImports(beast::Journal j)
{
testcase("Imports");
TestLogic logic(j);
Gossip g[5];
for (auto& gossip : g)
createGossip(gossip);
for (int i = 0; i < 5; ++i)
logic.importConsumers(std::to_string(i), g[i]);
pass();
}
void
testImport(beast::Journal j)
{
testcase("Import");
TestLogic logic(j);
Gossip g;
Gossip::Item item;
item.balance = 100;
beast::IP::AddressV4::bytes_type const d = {{192, 0, 2, 1}};
item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}};
g.items.push_back(item);
logic.importConsumers("g", g);
pass();
}
void
testCharges(beast::Journal j)
{
testcase("Charge");
TestLogic logic(j);
{
beast::IP::Endpoint const address(beast::IP::Endpoint::fromString("192.0.2.1"));
Consumer c(logic.newInboundEndpoint(address));
Charge const fee(1000);
JLOG(j.info()) << "Charging " << c.toString() << " " << fee << " per second";
c.charge(fee);
for (int i = 0; i < 128; ++i)
{
JLOG(j.info()) << "Time= " << logic.clock().now().time_since_epoch().count()
<< ", Balance = " << c.balance();
logic.advance();
}
}
{
beast::IP::Endpoint const address(beast::IP::Endpoint::fromString("192.0.2.2"));
Consumer c(logic.newInboundEndpoint(address));
Charge const fee(1000);
JLOG(j.info()) << "Charging " << c.toString() << " " << fee << " per second";
for (int i = 0; i < 128; ++i)
{
c.charge(fee);
JLOG(j.info()) << "Time= " << logic.clock().now().time_since_epoch().count()
<< ", Balance = " << c.balance();
logic.advance();
}
}
pass();
}
void
run() override
{
using beast::Severity;
test::SuiteJournal journal("ResourceManager_test", *this);
testDrop(journal, true);
testDrop(journal, false);
testCharges(journal);
testImports(journal);
testImport(journal);
}
};
BEAST_DEFINE_TESTSUITE(ResourceManager, resource, xrpl);
} // namespace xrpl::Resource

View File

@@ -7,6 +7,7 @@
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
@@ -16,7 +17,7 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/jss.h>
#include <cstdlib>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
@@ -173,7 +174,7 @@ public:
Oracle const oracle(
env,
{.owner = owner,
.documentID = rand(),
.documentID = randInt<std::uint32_t>(),
.series = {{"XRP", "USD", 740 + i, 1}, {"XRP", "EUR", 740, 1}},
.fee = baseFee});
oracles.emplace_back(owner, oracle.documentID());

View File

@@ -1,177 +0,0 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <chrono>
#include <cstdint>
#include <list>
#include <ostream>
#include <utility>
#include <vector>
namespace xrpl::tests {
class SHAMapSync_test : public beast::unit_test::Suite
{
public:
beast::xor_shift_engine eng;
boost::intrusive_ptr<SHAMapItem>
makeRandomAS()
{
Serializer s;
for (int d = 0; d < 3; ++d)
s.add32(randInt<std::uint32_t>(eng));
return makeShamapitem(s.getSHA512Half(), s.slice());
}
bool
confuseMap(SHAMap& map, int count)
{
// add a bunch of random states to a map, then remove them
// map should be the same
SHAMapHash const beforeHash = map.getHash();
std::list<uint256> items;
for (int i = 0; i < count; ++i)
{
auto item = makeRandomAS();
items.push_back(item->key());
if (!map.addItem(SHAMapNodeType::TnAccountState, item))
{
log << "Unable to add item to map\n";
return false;
}
}
for (auto const& item : items)
{
if (!map.delItem(item))
{
log << "Unable to remove item from map\n";
return false;
}
}
if (beforeHash != map.getHash())
{
log << "Hashes do not match " << beforeHash << " " << map.getHash() << std::endl;
return false;
}
return true;
}
void
run() override
{
using beast::Severity;
test::SuiteJournal journal("SHAMapSync_test", *this);
TestNodeFamily f(journal), f2(journal);
SHAMap source(SHAMapType::FREE, f);
SHAMap destination(SHAMapType::FREE, f2);
int const items = 10000;
for (int i = 0; i < items; ++i)
{
source.addItem(SHAMapNodeType::TnAccountState, makeRandomAS());
if (i % 100 == 0)
source.invariants();
}
source.invariants();
BEAST_EXPECT(confuseMap(source, 500));
source.invariants();
source.setImmutable();
int count = 0;
source.visitLeaves([&count](auto const& item) { ++count; });
BEAST_EXPECT(count == items);
std::vector<SHAMapMissingNode> missingNodes;
source.walkMap(missingNodes, 2048);
BEAST_EXPECT(missingNodes.empty());
destination.setSynching();
{
std::vector<std::pair<SHAMapNodeID, Blob>> a;
BEAST_EXPECT(source.getNodeFat(SHAMapNodeID(), a, randBool(eng), randInt(eng, 2)));
unexpected(a.empty(), "NodeSize");
BEAST_EXPECT(destination.addRootNode(source.getHash(), makeSlice(a[0].second), nullptr)
.isGood());
}
do
{
f.clock().advance(std::chrono::seconds(1));
// get the list of nodes we know we need
auto nodesMissing = destination.getMissingNodes(2048, nullptr);
if (nodesMissing.empty())
break;
// get as many nodes as possible based on this information
std::vector<std::pair<SHAMapNodeID, Blob>> b;
for (auto& it : nodesMissing)
{
// Don't use BEAST_EXPECT here b/c it will be called a
// non-deterministic number of times and the number of tests run
// should be deterministic
if (!source.getNodeFat(it.first, b, randBool(eng), randInt(eng, 2)))
fail("", __FILE__, __LINE__);
}
// Don't use BEAST_EXPECT here b/c it will be called a
// non-deterministic number of times and the number of tests run
// should be deterministic
if (b.empty())
fail("", __FILE__, __LINE__);
for (auto& node : b)
{
// Don't use BEAST_EXPECT here b/c it will be called a
// non-deterministic number of times and the number of tests run
// should be deterministic
if (!destination.addKnownNode(node.first, makeSlice(node.second), nullptr)
.isUseful())
fail("", __FILE__, __LINE__);
}
} while (true);
destination.clearSynching();
BEAST_EXPECT(source.deepCompare(destination));
destination.invariants();
}
};
BEAST_DEFINE_TESTSUITE(SHAMapSync, shamap, xrpl);
} // namespace xrpl::tests

View File

@@ -1,427 +0,0 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapInnerNode.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapLeafNode.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <memory>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl::tests {
#ifndef __INTELLISENSE__
static_assert(std::is_nothrow_destructible<SHAMap>{});
static_assert(!std::is_default_constructible<SHAMap>{});
static_assert(!std::is_copy_constructible<SHAMap>{});
static_assert(!std::is_copy_assignable<SHAMap>{});
static_assert(!std::is_move_constructible<SHAMap>{});
static_assert(!std::is_move_assignable<SHAMap>{});
static_assert(std::is_nothrow_destructible<SHAMap::ConstIterator>{});
static_assert(std::is_copy_constructible<SHAMap::ConstIterator>{});
static_assert(std::is_copy_assignable<SHAMap::ConstIterator>{});
static_assert(std::is_move_constructible<SHAMap::ConstIterator>{});
static_assert(std::is_move_assignable<SHAMap::ConstIterator>{});
static_assert(std::is_nothrow_destructible<SHAMapItem>{});
static_assert(!std::is_default_constructible<SHAMapItem>{});
static_assert(!std::is_copy_constructible<SHAMapItem>{});
static_assert(std::is_nothrow_destructible<SHAMapNodeID>{});
static_assert(std::is_default_constructible<SHAMapNodeID>{});
static_assert(std::is_copy_constructible<SHAMapNodeID>{});
static_assert(std::is_copy_assignable<SHAMapNodeID>{});
static_assert(std::is_move_constructible<SHAMapNodeID>{});
static_assert(std::is_move_assignable<SHAMapNodeID>{});
static_assert(std::is_nothrow_destructible<SHAMapHash>{});
static_assert(std::is_default_constructible<SHAMapHash>{});
static_assert(std::is_copy_constructible<SHAMapHash>{});
static_assert(std::is_copy_assignable<SHAMapHash>{});
static_assert(std::is_move_constructible<SHAMapHash>{});
static_assert(std::is_move_assignable<SHAMapHash>{});
static_assert(std::is_nothrow_destructible<SHAMapTreeNode>{});
static_assert(!std::is_default_constructible<SHAMapTreeNode>{});
static_assert(!std::is_copy_constructible<SHAMapTreeNode>{});
static_assert(!std::is_copy_assignable<SHAMapTreeNode>{});
static_assert(!std::is_move_constructible<SHAMapTreeNode>{});
static_assert(!std::is_move_assignable<SHAMapTreeNode>{});
static_assert(std::is_nothrow_destructible<SHAMapInnerNode>{});
static_assert(!std::is_default_constructible<SHAMapInnerNode>{});
static_assert(!std::is_copy_constructible<SHAMapInnerNode>{});
static_assert(!std::is_copy_assignable<SHAMapInnerNode>{});
static_assert(!std::is_move_constructible<SHAMapInnerNode>{});
static_assert(!std::is_move_assignable<SHAMapInnerNode>{});
static_assert(std::is_nothrow_destructible<SHAMapLeafNode>{});
static_assert(!std::is_default_constructible<SHAMapLeafNode>{});
static_assert(!std::is_copy_constructible<SHAMapLeafNode>{});
static_assert(!std::is_copy_assignable<SHAMapLeafNode>{});
static_assert(!std::is_move_constructible<SHAMapLeafNode>{});
static_assert(!std::is_move_assignable<SHAMapLeafNode>{});
#endif
inline bool
operator==(SHAMapItem const& a, SHAMapItem const& b)
{
return a.key() == b.key();
}
inline bool
operator!=(SHAMapItem const& a, SHAMapItem const& b)
{
return a.key() != b.key();
}
inline bool
operator==(SHAMapItem const& a, uint256 const& b)
{
return a.key() == b;
}
inline bool
operator!=(SHAMapItem const& a, uint256 const& b)
{
return a.key() != b;
}
class SHAMap_test : public beast::unit_test::Suite
{
public:
static Buffer
intToVuc(int v)
{
Buffer vuc(32);
std::fill_n(vuc.data(), vuc.size(), static_cast<std::uint8_t>(v));
return vuc;
}
void
run() override
{
using beast::Severity;
test::SuiteJournal journal("SHAMap_test", *this);
run(true, journal);
run(false, journal);
}
void
run(bool backed, beast::Journal const& journal)
{
if (backed)
{
testcase("add/traverse backed");
}
else
{
testcase("add/traverse unbacked");
}
tests::TestNodeFamily f(journal);
// h3 and h4 differ only in the leaf, same terminal node (level 19)
constexpr uint256 kH1("092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 kH2("436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 kH3("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 kH4("b92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 kH5("a92891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
SHAMap sMap(SHAMapType::FREE, f);
sMap.invariants();
if (!backed)
sMap.setUnbacked();
auto i1 = makeShamapitem(kH1, intToVuc(1));
auto i2 = makeShamapitem(kH2, intToVuc(2));
auto i3 = makeShamapitem(kH3, intToVuc(3));
auto i4 = makeShamapitem(kH4, intToVuc(4));
auto i5 = makeShamapitem(kH5, intToVuc(5));
unexpected(!sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i2)), "no add");
sMap.invariants();
unexpected(!sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i1)), "no add");
sMap.invariants();
auto i = sMap.begin();
auto e = sMap.end();
unexpected(i == e || (*i != *i1), "bad traverse");
++i;
unexpected(i == e || (*i != *i2), "bad traverse");
++i;
unexpected(i != e, "bad traverse");
sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i4));
sMap.invariants();
sMap.delItem(i2->key());
sMap.invariants();
sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i3));
sMap.invariants();
i = sMap.begin();
e = sMap.end();
unexpected(i == e || (*i != *i1), "bad traverse");
++i;
unexpected(i == e || (*i != *i3), "bad traverse");
++i;
unexpected(i == e || (*i != *i4), "bad traverse");
++i;
unexpected(i != e, "bad traverse");
if (backed)
{
testcase("snapshot backed");
}
else
{
testcase("snapshot unbacked");
}
SHAMapHash const mapHash = sMap.getHash();
std::shared_ptr<SHAMap> const map2 = sMap.snapShot(false);
map2->invariants();
unexpected(sMap.getHash() != mapHash, "bad snapshot");
unexpected(map2->getHash() != mapHash, "bad snapshot");
SHAMap::Delta delta;
BEAST_EXPECT(sMap.compare(*map2, delta, 100));
BEAST_EXPECT(delta.empty());
unexpected(!sMap.delItem(sMap.begin()->key()), "bad mod");
sMap.invariants();
unexpected(sMap.getHash() == mapHash, "bad snapshot");
unexpected(map2->getHash() != mapHash, "bad snapshot");
BEAST_EXPECT(sMap.compare(*map2, delta, 100));
BEAST_EXPECT(delta.size() == 1);
BEAST_EXPECT(delta.begin()->first == kH1);
BEAST_EXPECT(delta.begin()->second.first == nullptr);
BEAST_EXPECT(delta.begin()->second.second->key() == kH1);
sMap.dump();
if (backed)
{
testcase("build/tear backed");
}
else
{
testcase("build/tear unbacked");
}
{
static constexpr std::array keys{
uint256(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8")};
static constexpr std::array kHashes{
uint256(
"B7387CFEA0465759ADC718E8C42B52D2309D179B326E239EB5075C"
"64B6281F7F"),
uint256(
"FBC195A9592A54AB44010274163CB6BA95F497EC5BA0A883184546"
"7FB2ECE266"),
uint256(
"4E7D2684B65DFD48937FFB775E20175C43AF0C94066F7D5679F51A"
"E756795B75"),
uint256(
"7A2F312EB203695FFD164E038E281839EEF06A1B99BFC263F3CECC"
"6C74F93E07"),
uint256(
"395A6691A372387A703FB0F2C6D2C405DAF307D0817F8F0E207596"
"462B0E3A3E"),
uint256(
"D044C0A696DE3169CC70AE216A1564D69DE96582865796142CE7D9"
"8A84D9DDE4"),
uint256(
"76DCC77C4027309B5A91AD164083264D70B77B5E43E08AEDA5EBF9"
"4361143615"),
uint256(
"DF4220E93ADC6F5569063A01B4DC79F8DB9553B6A3222ADE23DEA0"
"2BBE7230E5")};
SHAMap map(SHAMapType::FREE, f);
if (!backed)
map.setUnbacked();
BEAST_EXPECT(map.getHash() == beast::kZero);
for (int k = 0; k < keys.size(); ++k)
{
BEAST_EXPECT(map.addItem(
SHAMapNodeType::TnTransactionNm, makeShamapitem(keys[k], intToVuc(k))));
BEAST_EXPECT(map.getHash().asUInt256() == kHashes[k]);
map.invariants();
}
for (int k = keys.size() - 1; k >= 0; --k)
{
BEAST_EXPECT(map.getHash().asUInt256() == kHashes[k]);
BEAST_EXPECT(map.delItem(keys[k]));
map.invariants();
}
BEAST_EXPECT(map.getHash() == beast::kZero);
}
if (backed)
{
testcase("iterate backed");
}
else
{
testcase("iterate unbacked");
}
{
static constexpr std::array keys{
uint256(
"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8"),
uint256(
"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e"
"5a772c6ca8")};
tests::TestNodeFamily tf{journal};
SHAMap map{SHAMapType::FREE, tf};
if (!backed)
map.setUnbacked();
for (auto const& k : keys)
{
map.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(k, intToVuc(0)));
map.invariants();
}
int h = 7;
for (auto const& k : map)
{
BEAST_EXPECT(k.key() == keys[h]);
--h;
}
}
}
};
class SHAMapPathProof_test : public beast::unit_test::Suite
{
void
run() override
{
test::SuiteJournal journal("SHAMapPathProof_test", *this);
tests::TestNodeFamily tf{journal};
SHAMap map{SHAMapType::FREE, tf};
map.setUnbacked();
uint256 key;
uint256 rootHash;
std::vector<Blob> goodPath;
for (unsigned char c = 1; c < 100; ++c)
{
uint256 k(c);
map.addItem(
SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()}));
map.invariants();
auto root = map.getHash().asUInt256();
auto path = map.getProofPath(k);
BEAST_EXPECT(path);
if (!path)
break;
BEAST_EXPECT(map.verifyProofPath(root, k, *path));
if (c == 1)
{
// extra node
path->insert(path->begin(), path->front());
BEAST_EXPECT(!map.verifyProofPath(root, k, *path));
// wrong key
uint256 const wrongKey(c + 1);
BEAST_EXPECT(!map.getProofPath(wrongKey));
}
if (c == 99)
{
key = k;
rootHash = root;
goodPath = std::move(*path);
}
}
// still good
BEAST_EXPECT(map.verifyProofPath(rootHash, key, goodPath));
// empty path
std::vector<Blob> badPath;
BEAST_EXPECT(!map.verifyProofPath(rootHash, key, badPath));
// too long
badPath = goodPath;
badPath.push_back(goodPath.back());
BEAST_EXPECT(!map.verifyProofPath(rootHash, key, badPath));
// bad node
badPath.clear();
badPath.emplace_back(100, 100);
BEAST_EXPECT(!map.verifyProofPath(rootHash, key, badPath));
// bad node type
badPath.clear();
badPath.push_back(goodPath.front());
badPath.front().back()--; // change node type
BEAST_EXPECT(!map.verifyProofPath(rootHash, key, badPath));
// all inner
badPath.clear();
badPath = goodPath;
badPath.erase(badPath.begin());
BEAST_EXPECT(!map.verifyProofPath(rootHash, key, badPath));
}
};
BEAST_DEFINE_TESTSUITE(SHAMap, shamap, xrpl);
BEAST_DEFINE_TESTSUITE(SHAMapPathProof, shamap, xrpl);
} // namespace xrpl::tests

View File

@@ -29,6 +29,8 @@ set(test_modules
basics
crypto
json
resource
shamap
tx
protocol_autogen
)

View File

@@ -0,0 +1,260 @@
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
namespace xrpl::test {
struct BufferTest : public ::testing::Test
{
static bool
sane(Buffer const& b)
{
if (b.empty())
return b.data() == nullptr;
return b.data() != nullptr;
}
};
TEST_F(BufferTest, buffer)
{
std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a,
0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c,
0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3};
Buffer const b0;
EXPECT_TRUE(sane(b0));
EXPECT_TRUE(b0.empty());
Buffer b1{0};
EXPECT_TRUE(sane(b1));
EXPECT_TRUE(b1.empty());
std::memcpy(b1.alloc(16), data, 16);
EXPECT_TRUE(sane(b1));
EXPECT_FALSE(b1.empty());
EXPECT_EQ(b1.size(), 16);
Buffer b2{b1.size()};
EXPECT_TRUE(sane(b2));
EXPECT_FALSE(b2.empty());
EXPECT_EQ(b2.size(), b1.size());
std::memcpy(b2.data(), data + 16, 16);
Buffer b3{data, sizeof(data)};
EXPECT_TRUE(sane(b3));
EXPECT_FALSE(b3.empty());
EXPECT_EQ(b3.size(), sizeof(data));
EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0);
// Check equality and inequality comparisons.
// For code readability, we want to use general
// EXPECT_TRUE instead of specific EXPECT_EQ etc.
EXPECT_TRUE(b0 == b0);
EXPECT_TRUE(b0 != b1);
EXPECT_TRUE(b1 == b1);
EXPECT_TRUE(b1 != b2);
EXPECT_TRUE(b2 != b3);
// Check copy constructors and copy assignments:
{
Buffer x{b0};
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
Buffer y{b1};
EXPECT_EQ(y, b1);
EXPECT_TRUE(sane(y));
x = b2;
EXPECT_EQ(x, b2);
EXPECT_TRUE(sane(x));
x = y;
EXPECT_EQ(x, y);
EXPECT_TRUE(sane(x));
y = b3;
EXPECT_EQ(y, b3);
EXPECT_TRUE(sane(y));
x = b0;
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
#endif
x = x;
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
y = y;
EXPECT_EQ(y, b3);
EXPECT_TRUE(sane(y));
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
}
// Check move constructor & move assignments:
{
static_assert(std::is_nothrow_move_constructible_v<Buffer>);
static_assert(std::is_nothrow_move_assignable_v<Buffer>);
{ // Move-construct from empty buf
Buffer x;
Buffer const y{std::move(x)};
EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(sane(y));
EXPECT_TRUE(y.empty());
EXPECT_EQ(x, y); // NOLINT(bugprone-use-after-move)
}
{ // Move-construct from non-empty buf
Buffer x{b1};
Buffer const y{std::move(x)};
EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, b1);
}
{ // Move assign empty buf to empty buf
Buffer x;
Buffer y;
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign non-empty buf to empty buf
Buffer x;
Buffer y{b1};
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b1);
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign empty buf to non-empty buf
Buffer x{b1};
Buffer y;
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
}
{ // Move assign non-empty buf to non-empty buf
Buffer x{b1};
Buffer y{b2};
Buffer z{b3};
x = std::move(y);
EXPECT_TRUE(sane(x));
EXPECT_FALSE(x.empty());
EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move)
x = std::move(z);
EXPECT_TRUE(sane(x));
EXPECT_FALSE(x.empty());
EXPECT_TRUE(sane(z)); // NOLINT(bugprone-use-after-move)
EXPECT_TRUE(z.empty()); // NOLINT(bugprone-use-after-move)
}
}
{
Buffer w{static_cast<Slice>(b0)};
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b0);
Buffer x{static_cast<Slice>(b1)};
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b1);
Buffer y{static_cast<Slice>(b2)};
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, b2);
Buffer z{static_cast<Slice>(b3)};
EXPECT_TRUE(sane(z));
EXPECT_EQ(z, b3);
// Assign empty slice to empty buffer
w = static_cast<Slice>(b0);
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b0);
// Assign non-empty slice to empty buffer
w = static_cast<Slice>(b1);
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b1);
// Assign non-empty slice to non-empty buffer
x = static_cast<Slice>(b2);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b2);
// Assign non-empty slice to non-empty buffer
y = static_cast<Slice>(z);
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, z);
// Assign empty slice to non-empty buffer:
z = static_cast<Slice>(b0);
EXPECT_TRUE(sane(z));
EXPECT_EQ(z, b0);
}
{
auto test = [](Buffer const& b, std::size_t i) {
Buffer x{b};
// Try to allocate some number of bytes, possibly
// zero (which means clear) and sanity check
x(i);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x.size(), i);
EXPECT_EQ((x.data() == nullptr), (i == 0));
// Try to allocate some more data (always non-zero)
x(i + 1);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x.size(), i + 1);
EXPECT_NE(x.data(), nullptr);
// Try to clear:
x.clear();
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_EQ(x.data(), nullptr);
// Try to clear again:
x.clear();
EXPECT_TRUE(sane(x));
EXPECT_TRUE(x.empty());
EXPECT_EQ(x.data(), nullptr);
};
for (std::size_t i = 0; i < 16; ++i)
{
test(b0, i);
test(b1, i);
}
}
}
} // namespace xrpl::test

View File

@@ -0,0 +1,94 @@
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/ByteUtilities.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
#include <gtest/gtest.h>
#include <fstream>
#include <stdexcept>
#include <string>
namespace xrpl {
namespace {
class TempFile
{
public:
explicit TempFile(boost::filesystem::path file, std::string const& contents)
: dir_(
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("xrpl-file-utilities-%%%%-%%%%-%%%%"))
, file_(dir_ / file)
{
boost::filesystem::create_directory(dir_);
std::ofstream output(file_.string());
if (!output)
throw std::runtime_error("Unable to create temporary test file");
output << contents;
}
~TempFile()
{
boost::system::error_code ec;
boost::filesystem::remove(file_, ec);
boost::filesystem::remove(dir_, ec);
}
[[nodiscard]] boost::filesystem::path const&
file() const
{
return file_;
}
private:
boost::filesystem::path dir_;
boost::filesystem::path file_;
};
} // namespace
TEST(FileUtilitiesTest, get_file_contents)
{
using namespace boost::system;
constexpr char const* kExpectedContents = "This file is very short. That's all we need.";
TempFile const file("test_file", "This is temporary text that should get overwritten");
error_code ec;
auto const path = file.file();
writeFileContents(ec, path, kExpectedContents);
EXPECT_FALSE(ec);
{
// Test with no max
auto const good = getFileContents(ec, path);
EXPECT_FALSE(ec);
EXPECT_EQ(good, kExpectedContents);
}
{
// Test with large max
auto const good = getFileContents(ec, path, kilobytes(1));
EXPECT_FALSE(ec);
EXPECT_EQ(good, kExpectedContents);
}
{
// Test with small max
auto const bad = getFileContents(ec, path, 16);
EXPECT_TRUE(ec && ec.value() == boost::system::errc::file_too_large);
EXPECT_TRUE(bad.empty());
}
}
} // namespace xrpl

View File

@@ -0,0 +1,243 @@
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
namespace xrpl {
TEST(IOUAmountTest, zero)
{
IOUAmount const z(0, 0);
EXPECT_EQ(z.mantissa(), 0);
EXPECT_EQ(z.exponent(), -100);
EXPECT_FALSE(z);
EXPECT_EQ(z.signum(), 0);
EXPECT_EQ(z, beast::kZero);
EXPECT_EQ((z + z), z);
EXPECT_EQ((z - z), z);
EXPECT_EQ(z, -z);
IOUAmount const zz(beast::kZero);
EXPECT_EQ(z, zz);
// https://github.com/XRPLF/rippled/issues/5170
IOUAmount const zzz{};
EXPECT_EQ(zzz, beast::kZero);
// EXPECT_EQ(zzz, zz);
}
TEST(IOUAmountTest, sig_num)
{
IOUAmount const neg(-1, 0);
EXPECT_LT(neg.signum(), 0);
IOUAmount const zer(0, 0);
EXPECT_EQ(zer.signum(), 0);
IOUAmount const pos(1, 0);
EXPECT_GT(pos.signum(), 0);
}
TEST(IOUAmountTest, beast_zero)
{
using beast::kZero;
{
IOUAmount const z(kZero);
EXPECT_TRUE(z == kZero);
EXPECT_TRUE(z >= kZero);
EXPECT_TRUE(z <= kZero);
EXPECT_FALSE(z != kZero);
EXPECT_FALSE(z > kZero);
EXPECT_FALSE(z < kZero);
}
{
IOUAmount const neg(-2, 0);
EXPECT_TRUE(neg < kZero);
EXPECT_TRUE(neg <= kZero);
EXPECT_TRUE(neg != kZero);
EXPECT_FALSE(neg == kZero);
}
{
IOUAmount const pos(2, 0);
EXPECT_TRUE(pos > kZero);
EXPECT_TRUE(pos >= kZero);
EXPECT_TRUE(pos != kZero);
EXPECT_FALSE(pos == kZero);
}
}
TEST(IOUAmountTest, comparisons)
{
IOUAmount const n(-2, 0);
IOUAmount const z(0, 0);
IOUAmount const p(2, 0);
// For code readability, we want to use general
// EXPECT_TRUE instead of specific EXPECT_EQ etc.
EXPECT_TRUE(z == z);
EXPECT_TRUE(z >= z);
EXPECT_TRUE(z <= z);
EXPECT_TRUE(z == -z);
// NOLINTBEGIN(misc-redundant-expression)
EXPECT_FALSE(z > z);
EXPECT_FALSE(z < z);
EXPECT_FALSE(z != z);
// NOLINTEND(misc-redundant-expression)
EXPECT_FALSE(z != -z);
EXPECT_TRUE(n < z);
EXPECT_TRUE(n <= z);
EXPECT_TRUE(n != z);
EXPECT_FALSE(n > z);
EXPECT_FALSE(n >= z);
EXPECT_FALSE(n == z);
EXPECT_TRUE(p > z);
EXPECT_TRUE(p >= z);
EXPECT_TRUE(p != z);
EXPECT_FALSE(p < z);
EXPECT_FALSE(p <= z);
EXPECT_FALSE(p == z);
EXPECT_TRUE(n < p);
EXPECT_TRUE(n <= p);
EXPECT_TRUE(n != p);
EXPECT_FALSE(n > p);
EXPECT_FALSE(n >= p);
EXPECT_FALSE(n == p);
EXPECT_TRUE(p > n);
EXPECT_TRUE(p >= n);
EXPECT_TRUE(p != n);
EXPECT_FALSE(p < n);
EXPECT_FALSE(p <= n);
EXPECT_FALSE(p == n);
EXPECT_TRUE(p > -p);
EXPECT_TRUE(p >= -p);
EXPECT_TRUE(p != -p);
EXPECT_TRUE(n < -n);
EXPECT_TRUE(n <= -n);
EXPECT_TRUE(n != -n);
}
TEST(IOUAmountTest, to_string)
{
auto test = [](IOUAmount const& n, std::string const& expected) {
auto const result = to_string(n);
std::stringstream ss;
ss << "to_string(" << result << "). Expected: " << expected;
EXPECT_EQ(result, expected) << ss.str();
};
for (auto const mantissaSize : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg(mantissaSize);
test(IOUAmount(-2, 0), "-2");
test(IOUAmount(0, 0), "0");
test(IOUAmount(2, 0), "2");
test(IOUAmount(25, -3), "0.025");
test(IOUAmount(-25, -3), "-0.025");
test(IOUAmount(25, 1), "250");
test(IOUAmount(-25, 1), "-250");
test(IOUAmount(2, 20), "2e20");
test(IOUAmount(-2, -20), "-2e-20");
}
}
TEST(IOUAmountTest, mul_ratio)
{
/* The range for the mantissa when normalized */
constexpr std::int64_t kMinMantissa = 1000000000000000ull;
constexpr std::int64_t kMaxMantissa = 9999999999999999ull;
// log(2,maxMantissa) ~ 53.15
/* The range for the exponent when normalized */
constexpr int kMinExponent = -96;
constexpr int kMaxExponent = 80;
constexpr auto kMaxUInt = std::numeric_limits<std::uint32_t>::max();
{
// multiply by a number that would overflow the mantissa, then
// divide by the same number, and check we didn't lose any value
IOUAmount const bigMan(kMaxMantissa, 0);
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false));
}
{
// Similar test as above, but for negative values
IOUAmount const bigMan(-kMaxMantissa, 0);
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false));
}
{
// small amounts
IOUAmount const tiny(kMinMantissa, kMinExponent);
// Round up should give the smallest allowable number
EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt, true));
EXPECT_EQ(tiny, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true));
// rounding down should be zero
EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt, false));
EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false));
// tiny negative numbers
IOUAmount const tinyNeg(-kMinMantissa, kMinExponent);
// Round up should give zero
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt, true));
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true));
// rounding down should be tiny
EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, 1, kMaxUInt, false));
EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false));
}
{ // rounding
{
IOUAmount const one(1, 0);
auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false);
EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1);
}
{
IOUAmount const big(kMaxMantissa, kMaxExponent);
auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false);
EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1);
}
{
IOUAmount const negOne(-1, 0);
auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true);
auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false);
EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1);
}
}
{
// division by zero
IOUAmount const one(1, 0);
EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); });
}
{
// overflow
IOUAmount const big(kMaxMantissa, kMaxExponent);
EXPECT_ANY_THROW({ mulRatio(big, 2, 0, true); });
}
}
} // namespace xrpl

View File

@@ -0,0 +1,844 @@
#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono> // IWYU pragma: keep
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <latch>
#include <mutex>
#include <optional>
#include <random>
#include <stdexcept>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
namespace xrpl::tests {
/*
* Experimentally, we discovered that using std::barrier performs extremely
* poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS
* environments. To unblock our macOS CI pipeline, we replaced std::barrier with a
* custom mutex-based barrier (Barrier) that significantly improves performance
* without compromising correctness. For future reference, if we ever consider
* reintroducing std::barrier, the following configuration is known to exhibit the
* problem:
*
* Model Name: Mac mini
* Model Identifier: Mac14,3
* Model Number: Z16K000R4LL/A
* Chip: Apple M2
* Total Number of Cores: 8 (4 performance and 4 efficiency)
* Memory: 24 GB
* System Firmware Version: 11881.41.5
* OS Loader Version: 11881.1.1
* Apple clang version 16.0.0 (clang-1600.0.26.3)
* Target: arm64-apple-darwin24.0.0
* Thread model: posix
*
*/
struct Barrier
{
std::mutex mtx;
std::condition_variable cv;
int count;
int const initial;
std::size_t generation{0};
explicit Barrier(int n) : count(n), initial(n)
{
}
void
arriveAndWait()
{
std::unique_lock lock(mtx);
auto const currentGeneration = generation;
if (--count == 0)
{
++generation;
count = initial;
cv.notify_all();
}
else
{
cv.wait(lock, [&] { return generation != currentGeneration; });
}
}
};
namespace {
enum class TrackedState : std::uint8_t {
Uninitialized,
Alive,
PartiallyDeletedStarted,
PartiallyDeleted,
DeletedStarted,
Deleted
};
class TIBase : public IntrusiveRefCounts
{
public:
static constexpr std::size_t kMaxStates = 128;
static std::array<std::atomic<TrackedState>, kMaxStates> state;
static std::atomic<std::size_t> nextId;
static TrackedState
getState(std::size_t id)
{
if (id >= state.size())
throw std::out_of_range("TIBase state id out of range");
return state[id].load(std::memory_order_acquire);
}
static void
resetStates(bool resetCallback)
{
for (std::size_t i = 0; i < kMaxStates; ++i)
{
state[i].store(TrackedState::Uninitialized, std::memory_order_release);
}
nextId.store(0, std::memory_order_release);
if (resetCallback)
TIBase::tracingCallback = [](TrackedState, std::optional<TrackedState>) {};
}
struct ResetStatesGuard
{
bool resetCallback{false};
ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback}
{
TIBase::resetStates(resetCallback);
}
~ResetStatesGuard()
{
TIBase::resetStates(resetCallback);
}
};
TIBase() : id{checkoutID()}
{
state[id].store(TrackedState::Alive, std::memory_order_relaxed);
}
~TIBase() override
{
using enum TrackedState;
tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted);
// Use relaxed memory order to try to avoid atomic operations from
// adding additional memory synchronizations that may hide threading
// errors in the underlying shared pointer class.
state[id].store(DeletedStarted, std::memory_order_relaxed);
tracingCallback(DeletedStarted, Deleted);
state[id].store(TrackedState::Deleted, std::memory_order_relaxed);
tracingCallback(TrackedState::Deleted, std::nullopt);
}
void
partialDestructor() const
{
using enum TrackedState;
tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted);
state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed);
tracingCallback(PartiallyDeletedStarted, PartiallyDeleted);
state[id].store(PartiallyDeleted, std::memory_order_relaxed);
tracingCallback(PartiallyDeleted, std::nullopt);
}
static std::function<void(TrackedState, std::optional<TrackedState>)> tracingCallback;
std::size_t const id;
private:
static std::size_t
checkoutID()
{
auto const id = nextId.fetch_add(1, std::memory_order_acq_rel);
if (id >= state.size())
throw std::out_of_range("TIBase state capacity exceeded");
return id;
}
};
std::array<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
std::atomic<std::size_t> TIBase::nextId{0};
std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
[](TrackedState, std::optional<TrackedState>) {};
} // namespace
TEST(IntrusiveSharedTest, basics)
{
{
TIBase::ResetStatesGuard const rsg{true};
TIBase const b;
EXPECT_EQ(b.useCount(), 1);
b.addWeakRef();
EXPECT_EQ(b.useCount(), 1);
auto s = b.releaseStrongRef();
EXPECT_EQ(s, ReleaseStrongRefAction::PartialDestroy);
EXPECT_EQ(b.useCount(), 0);
TIBase const* pb = &b;
partialDestructorFinished(&pb);
EXPECT_FALSE(pb);
auto w = b.releaseWeakRef();
EXPECT_EQ(w, ReleaseWeakRefAction::Destroy);
}
std::vector<SharedIntrusive<TIBase>> strong;
std::vector<WeakIntrusive<TIBase>> weak;
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
auto id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
EXPECT_EQ(b->useCount(), 1);
for (int i = 0; i < 10; ++i)
{
strong.push_back(b);
}
b.reset();
EXPECT_EQ(TIBase::getState(id), Alive);
strong.resize(strong.size() - 1);
EXPECT_EQ(TIBase::getState(id), Alive);
strong.clear();
EXPECT_EQ(TIBase::getState(id), Deleted);
b = makeSharedIntrusive<TIBase>();
id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
EXPECT_EQ(b->useCount(), 1);
for (int i = 0; i < 10; ++i)
{
weak.emplace_back(b);
EXPECT_EQ(b->useCount(), 1);
}
EXPECT_EQ(TIBase::getState(id), Alive);
weak.resize(weak.size() - 1);
EXPECT_EQ(TIBase::getState(id), Alive);
b.reset();
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
while (!weak.empty())
{
weak.resize(weak.size() - 1);
if (!weak.empty())
{
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
}
}
EXPECT_EQ(TIBase::getState(id), Deleted);
}
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
auto id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
WeakIntrusive<TIBase> w{b};
EXPECT_EQ(TIBase::getState(id), Alive);
auto s = w.lock();
EXPECT_TRUE(s && s->useCount() == 2);
b.reset();
EXPECT_TRUE(TIBase::getState(id) == Alive);
EXPECT_TRUE(s && s->useCount() == 1);
s.reset();
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
EXPECT_TRUE(w.expired());
s = w.lock();
// Cannot convert a weak pointer to a strong pointer if object is
// already partially deleted
EXPECT_FALSE(s);
w.reset();
EXPECT_EQ(TIBase::getState(id), Deleted);
}
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
using swu = SharedWeakUnion<TIBase>;
swu b = makeSharedIntrusive<TIBase>();
EXPECT_TRUE(b.isStrong() && b.useCount() == 1);
auto id = b.get()->id;
EXPECT_EQ(TIBase::getState(id), Alive);
swu w = b;
EXPECT_TRUE(TIBase::getState(id) == Alive);
EXPECT_TRUE(w.isStrong() && b.useCount() == 2);
w.convertToWeak();
EXPECT_TRUE(w.isWeak() && b.useCount() == 1);
swu s = w;
EXPECT_TRUE(s.isWeak() && b.useCount() == 1);
s.convertToStrong();
EXPECT_TRUE(s.isStrong() && b.useCount() == 2);
b.reset();
EXPECT_EQ(TIBase::getState(id), Alive);
EXPECT_EQ(s.useCount(), 1);
EXPECT_FALSE(w.expired());
s.reset();
EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
EXPECT_TRUE(w.expired());
w.convertToStrong();
// Cannot convert a weak pointer to a strong pointer if object is
// already partially deleted
EXPECT_TRUE(w.isWeak());
w.reset();
EXPECT_EQ(TIBase::getState(id), Deleted);
}
{
// Testing SharedWeakUnion assignment operator
TIBase::ResetStatesGuard const rsg{true};
auto strong1 = makeSharedIntrusive<TIBase>();
auto strong2 = makeSharedIntrusive<TIBase>();
auto id1 = strong1->id;
auto id2 = strong2->id;
EXPECT_NE(id1, id2);
SharedWeakUnion<TIBase> union1 = strong1;
SharedWeakUnion<TIBase> union2 = strong2;
EXPECT_TRUE(union1.isStrong());
EXPECT_TRUE(union2.isStrong());
EXPECT_EQ(union1.get(), strong1.get());
EXPECT_EQ(union2.get(), strong2.get());
// 1) Normal assignment: explicitly calls SharedWeakUnion assignment
union1 = union2;
EXPECT_TRUE(union1.isStrong());
EXPECT_TRUE(union2.isStrong());
EXPECT_EQ(union1.get(), union2.get());
EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
EXPECT_EQ(TIBase::getState(id2), TrackedState::Alive);
// 2) Test self-assignment
EXPECT_TRUE(union1.isStrong());
EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
int const initialRefCount = strong1->useCount();
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
union1 = union1; // Self-assignment
#pragma clang diagnostic pop
EXPECT_TRUE(union1.isStrong());
EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive);
EXPECT_EQ(strong1->useCount(), initialRefCount);
// 3) Test assignment from null union pointer
union1 = SharedWeakUnion<TIBase>();
EXPECT_EQ(union1.get(), nullptr);
// 4) Test assignment to expired union pointer
strong2.reset();
union2.reset();
union1 = union2;
EXPECT_EQ(union1.get(), nullptr);
EXPECT_EQ(TIBase::getState(id2), TrackedState::Deleted);
}
}
TEST(IntrusiveSharedTest, partial_delete)
{
// This test creates two threads. One with a strong pointer and one
// with a weak pointer. The strong pointer is reset while the weak
// pointer still holds a reference, triggering a partial delete.
// While the partial delete function runs (a sleep is inserted) the
// weak pointer is reset. The destructor should wait to run until
// after the partial delete function has completed running.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
auto strong = makeSharedIntrusive<TIBase>();
WeakIntrusive<TIBase> weak{strong};
std::atomic<bool> destructorRan{false};
std::atomic<bool> partialDeleteRan{false};
std::latch partialDeleteStartedSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
if (next == DeletedStarted)
{
// strong goes out of scope while weak is still in scope
// This checks that partialDelete has run to completion
// before the destructor is called. A sleep is inserted
// inside the partial delete to make sure the destructor is
// given an opportunity to run during partial delete.
EXPECT_EQ(cur, PartiallyDeleted);
}
if (next == PartiallyDeletedStarted)
{
partialDeleteStartedSyncPoint.arrive_and_wait();
using namespace std::chrono_literals;
// Sleep and let the weak pointer go out of scope,
// potentially triggering a destructor while partial delete
// is running. The test is to make sure that doesn't happen.
std::this_thread::sleep_for(800ms);
}
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan.exchange(true));
}
};
std::thread t1{[&] {
partialDeleteStartedSyncPoint.arrive_and_wait();
weak.reset(); // Trigger a full delete as soon as the partial
// delete starts
}};
std::thread t2{[&] {
strong.reset(); // Trigger a partial delete
}};
t1.join();
t2.join();
EXPECT_TRUE(destructorRan.load() && partialDeleteRan.load());
}
TEST(IntrusiveSharedTest, destructor)
{
// This test creates two threads. One with a strong pointer and one
// with a weak pointer. The weak pointer is reset while the strong
// pointer still holds a reference. Then the strong pointer is
// reset. Only the destructor should run. The partial destructor
// should not be called. Since the weak reset runs to completion
// before the strong pointer is reset, threading doesn't add much to
// this test, but there is no harm in keeping it.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
auto strong = makeSharedIntrusive<TIBase>();
WeakIntrusive<TIBase> weak{strong};
std::atomic<bool> destructorRan{false};
std::atomic<bool> partialDeleteRan{false};
std::latch weakResetSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load());
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan.exchange(true));
}
};
std::thread t1{[&] {
weak.reset();
weakResetSyncPoint.arrive_and_wait();
}};
std::thread t2{[&] {
weakResetSyncPoint.arrive_and_wait();
strong.reset(); // Trigger a partial delete
}};
t1.join();
t2.join();
EXPECT_TRUE(destructorRan.load() && !partialDeleteRan.load());
}
TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant)
{
// This test creates and destroys many strong and weak pointers in a
// loop. There is a random mix of strong and weak pointers stored in
// a vector (held as a variant). Both threads clear all the pointers
// and check that the invariants hold.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan || destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan);
setDestructorRan();
}
};
auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng)
-> std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> {
std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> result;
std::uniform_int_distribution<> toCreateDist(4, 64);
std::uniform_int_distribution<> isStrongDist(0, 1);
auto numToCreate = toCreateDist(eng);
result.reserve(numToCreate);
for (int i = 0; i < numToCreate; ++i)
{
if (isStrongDist(eng))
{
result.emplace_back(SharedIntrusive<TIBase>(toClone));
}
else
{
result.emplace_back(WeakIntrusive<TIBase>(toClone));
}
}
return result;
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> result;
result.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
result.emplace_back(rd());
return result;
}();
// cloneAndDestroy clones the strong pointer into a vector of mixed
// strong and weak pointers and destroys them all at once.
// threadId==0 is special.
auto cloneAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// Thread 0 is the genesis thread. It creates the strong
// pointers to be cloned by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
EXPECT_TRUE(i == 0 || destructorRan);
destructionState.store(0, std::memory_order_release);
toClone.clear();
toClone.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toClone, strong);
}
// ------ Sync Point ------
postCreateToCloneSyncPoint.arriveAndWait();
auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
toClone[threadId].reset();
// ------ Sync Point ------
postCreateVecOfPointersSyncPoint.arriveAndWait();
v.clear();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(cloneAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union)
{
// This test creates and destroys many SharedWeak pointers in a
// loop. All the pointers start as strong and a loop randomly
// convert them between strong and weak pointers. Both threads clear
// all the pointers and check that the invariants hold.
//
// Note: This test also differs from the test above in that the pointers
// randomly change from strong to weak and from weak to strong in a
// loop. This can't be done in the variant test above because variant is
// not thread safe while the SharedWeakUnion is thread safe.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan || destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan);
setDestructorRan();
}
};
auto createVecOfPointers =
[&](auto const& toClone,
std::default_random_engine& eng) -> std::vector<SharedWeakUnion<TIBase>> {
std::vector<SharedWeakUnion<TIBase>> result;
std::uniform_int_distribution<> toCreateDist(4, 64);
auto numToCreate = toCreateDist(eng);
result.reserve(numToCreate);
for (int i = 0; i < numToCreate; ++i)
result.emplace_back(SharedIntrusive<TIBase>(toClone));
return result;
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kFlipPointersLoopIters = 256;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
Barrier postFlipPointersLoopSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> result;
result.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
result.emplace_back(rd());
return result;
}();
// cloneAndDestroy clones the strong pointer into a vector of
// mixed strong and weak pointers, runs a loop that randomly
// changes strong pointers to weak pointers, and destroys them
// all at once.
auto cloneAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// threadId 0 is the genesis thread. It creates the
// strong point to be cloned by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
EXPECT_TRUE(i == 0 || destructorRan);
destructionState.store(0, std::memory_order_release);
toClone.clear();
toClone.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toClone, strong);
}
// ------ Sync Point ------
postCreateToCloneSyncPoint.arriveAndWait();
auto v = createVecOfPointers(toClone[threadId], engines[threadId]);
toClone[threadId].reset();
// ------ Sync Point ------
postCreateVecOfPointersSyncPoint.arriveAndWait();
std::uniform_int_distribution<> isStrongDist(0, 1);
for (int f = 0; f < kFlipPointersLoopIters; ++f)
{
for (auto& p : v)
{
if (isStrongDist(engines[threadId]))
{
p.convertToStrong();
}
else
{
p.convertToWeak();
}
}
}
// ------ Sync Point ------
postFlipPointersLoopSyncPoint.arriveAndWait();
v.clear();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(cloneAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
TEST(IntrusiveSharedTest, multithreaded_locking_weak)
{
// This test creates a single shared atomic pointer that multiple thread
// create weak pointers from. The threads then lock the weak pointers.
// Both threads clear all the pointers and check that the invariants
// hold.
using enum TrackedState;
TIBase::ResetStatesGuard const rsg{true};
std::atomic<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
int const s = destructionState.load(std::memory_order_relaxed);
return {(s & 1) != 0, (s & 2) != 0};
};
auto setDestructorRan = [&]() -> void {
destructionState.fetch_or(1, std::memory_order_acq_rel);
};
auto setPartialDeleteRan = [&]() -> void {
destructionState.fetch_or(2, std::memory_order_acq_rel);
};
auto tracingCallback = [&](TrackedState cur, std::optional<TrackedState> next) {
using enum TrackedState;
auto [destructorRan, partialDeleteRan] = getDestructorState();
if (next == PartiallyDeleted)
{
EXPECT_FALSE(partialDeleteRan || destructorRan);
setPartialDeleteRan();
}
if (next == Deleted)
{
EXPECT_FALSE(destructorRan);
setDestructorRan();
}
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kLockWeakLoopIters = 256;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toLock;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToLockSyncPoint{kNumThreads};
Barrier postLockWeakLoopSyncPoint{kNumThreads};
// lockAndDestroy creates weak pointers from the strong pointer
// and runs a loop that locks the weak pointer. At the end of the loop
// all the pointers are destroyed all at once.
auto lockAndDestroy = [&](int threadId) {
for (int i = 0; i < kLoopIters; ++i)
{
// ------ Sync Point ------
loopStartSyncPoint.arriveAndWait();
// only thread 0 should reset the state
std::optional<TIBase::ResetStatesGuard> rsg;
if (threadId == 0)
{
// threadId 0 is the genesis thread. It creates the
// strong point to be locked by the other threads. This
// thread will also check that the destructor ran and
// clear the temporary variables.
rsg.emplace(false);
auto [destructorRan, partialDeleteRan] = getDestructorState();
EXPECT_TRUE(i == 0 || destructorRan);
destructionState.store(0, std::memory_order_release);
toLock.clear();
toLock.resize(kNumThreads);
auto strong = makeSharedIntrusive<TIBase>();
strong->tracingCallback = tracingCallback;
std::ranges::fill(toLock, strong);
}
// ------ Sync Point ------
postCreateToLockSyncPoint.arriveAndWait();
// Multiple threads all create a weak pointer from the same
// strong pointer
WeakIntrusive const weak{toLock[threadId]};
for (int wi = 0; wi < kLockWeakLoopIters; ++wi)
{
EXPECT_FALSE(weak.expired());
auto strong = weak.lock();
EXPECT_TRUE(strong);
}
// ------ Sync Point ------
postLockWeakLoopSyncPoint.arriveAndWait();
toLock[threadId].reset();
}
};
std::vector<std::thread> threads;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i)
{
threads.emplace_back(lockAndDestroy, i);
}
for (int i = 0; i < kNumThreads; ++i)
{
threads[i].join();
}
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,81 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <string>
namespace xrpl {
class KeyCacheTest : public ::testing::Test
{
public:
};
TEST_F(KeyCacheTest, key_cache)
{
using namespace std::chrono_literals;
TestStopwatch clock;
clock.set(0);
using Key = std::string;
using Cache = TaggedCache<Key, int, true>;
beast::Journal const j{TestSink::instance()};
// Insert an item, retrieve it, and age it so it gets purged.
{
Cache c("test", LedgerIndex(1), 2s, clock, j);
EXPECT_EQ(c.size(), 0);
EXPECT_TRUE(c.insert("one"));
EXPECT_FALSE(c.insert("one"));
EXPECT_EQ(c.size(), 1);
EXPECT_TRUE(c.touchIfExists("one"));
++clock;
c.sweep();
EXPECT_EQ(c.size(), 1);
++clock;
c.sweep();
EXPECT_EQ(c.size(), 0);
EXPECT_FALSE(c.touchIfExists("one"));
}
// Insert two items, have one expire
{
Cache c("test", LedgerIndex(2), 2s, clock, j);
EXPECT_TRUE(c.insert("one"));
EXPECT_EQ(c.size(), 1);
EXPECT_TRUE(c.insert("two"));
EXPECT_EQ(c.size(), 2);
++clock;
c.sweep();
EXPECT_EQ(c.size(), 2);
EXPECT_TRUE(c.touchIfExists("two"));
++clock;
c.sweep();
EXPECT_EQ(c.size(), 1);
}
// Insert three items (1 over limit), sweep
{
Cache c("test", LedgerIndex(2), 3s, clock, j);
EXPECT_TRUE(c.insert("one"));
++clock;
EXPECT_TRUE(c.insert("two"));
++clock;
EXPECT_TRUE(c.insert("three"));
++clock;
EXPECT_EQ(c.size(), 3);
c.sweep();
EXPECT_LT(c.size(), 3);
}
}
} // namespace xrpl

View File

@@ -0,0 +1,293 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/ToString.h>
#include <gtest/gtest.h>
#include <string>
namespace xrpl {
class StringUtilitiesTest : public ::testing::Test
{
public:
static void
testUnHexSuccess(std::string const& strIn, std::string const& strExpected)
{
auto rv = strUnHex(strIn);
EXPECT_TRUE(rv);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(makeSlice(*rv), makeSlice(strExpected));
}
static void
testUnHexFailure(std::string const& strIn)
{
auto rv = strUnHex(strIn);
EXPECT_FALSE(rv);
}
};
TEST_F(StringUtilitiesTest, un_hex)
{
testUnHexSuccess("526970706c6544", "RippleD");
testUnHexSuccess("A", "\n");
testUnHexSuccess("0A", "\n");
testUnHexSuccess("D0A", "\r\n");
testUnHexSuccess("0D0A", "\r\n");
testUnHexSuccess("200D0A", " \r\n");
testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)");
// Check for things which contain some or only invalid characters
testUnHexFailure("123X");
testUnHexFailure("V");
testUnHexFailure("XRP");
}
TEST_F(StringUtilitiesTest, parse_url)
{
// Expected passes.
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_TRUE(pUrl.domain.empty());
EXPECT_FALSE(pUrl.port);
// RFC 3986:
// > In general, a URI that uses the generic syntax for authority
// with an empty path should be normalized to a path of "/".
// Do we want to normalize paths?
EXPECT_TRUE(pUrl.path.empty());
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme:///"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_TRUE(pUrl.domain.empty());
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "lower://domain"));
EXPECT_EQ(pUrl.scheme, "lower");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_TRUE(pUrl.path.empty());
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "UPPER://domain:234/"));
EXPECT_EQ(pUrl.scheme, "upper");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 234); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "Mixed://domain/path"));
EXPECT_EQ(pUrl.scheme, "mixed");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://[::1]:123/path"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "::1");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/path");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://domain:123/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/abc:321"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/abc:321");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme:///path/to/file"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_TRUE(pUrl.domain.empty());
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path/to/file");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_EQ(pUrl.username, "user");
EXPECT_EQ(pUrl.password, "pass");
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path/with/an@sign");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/path/with/an@sign"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "domain");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/path/with/an@sign");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "scheme://:999/"));
EXPECT_EQ(pUrl.scheme, "scheme");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, ":999");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/");
}
{
ParsedUrl pUrl;
EXPECT_TRUE(parseUrl(pUrl, "http://::1:1234/validators"));
EXPECT_EQ(pUrl.scheme, "http");
EXPECT_TRUE(pUrl.username.empty());
EXPECT_TRUE(pUrl.password.empty());
EXPECT_EQ(pUrl.domain, "::0.1.18.52");
EXPECT_FALSE(pUrl.port);
EXPECT_EQ(pUrl.path, "/validators");
}
// Expected fails.
{
ParsedUrl pUrl;
EXPECT_FALSE(parseUrl(pUrl, ""));
EXPECT_FALSE(parseUrl(pUrl, "nonsense"));
EXPECT_FALSE(parseUrl(pUrl, "://"));
EXPECT_FALSE(parseUrl(pUrl, ":///"));
EXPECT_FALSE(parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:23498765/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:0/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:+7/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:-7234/"));
EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:@#$56!/"));
}
{
std::string const strUrl("s://" + std::string(8192, ':'));
ParsedUrl pUrl;
EXPECT_FALSE(parseUrl(pUrl, strUrl));
}
}
TEST_F(StringUtilitiesTest, to_string)
{
auto result = to_string("hello");
EXPECT_EQ(result, "hello");
}
} // namespace xrpl

View File

@@ -0,0 +1,246 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <memory>
#include <string>
#include <utility>
namespace xrpl {
/*
I guess you can put some items in, make sure they're still there. Let some
time pass, make sure they're gone. Keep a strong pointer to one of them, make
sure you can still find it even after time passes. Create two objects with
the same key, canonicalize them both and make sure you get the same object.
Put an object in but keep a strong pointer to it, advance the clock a lot,
then canonicalize a new object with the same key, make sure you get the
original object.
*/
TEST(TaggedCacheTest, tagged_cache)
{
using namespace std::chrono_literals;
beast::Journal const journal{TestSink::instance()};
TestStopwatch clock;
clock.set(0);
using Key = LedgerIndex;
using Value = std::string;
using Cache = TaggedCache<Key, Value>;
Cache c("test", 1, 1s, clock, journal);
// Insert an item, retrieve it, and age it so it gets purged.
{
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
EXPECT_FALSE(c.insert(1, "one"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
{
std::string s;
EXPECT_TRUE(c.retrieve(1, s));
EXPECT_EQ(s, "one");
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
// Insert an item, maintain a strong pointer, age it, and
// verify that the entry still exists.
{
EXPECT_FALSE(c.insert(2, "two"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
{
auto p = c.fetch(2);
EXPECT_NE(p, nullptr);
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 1);
}
// Make sure its gone now that our reference is gone
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
// Insert the same key/value pair and make sure we get the same result
{
EXPECT_FALSE(c.insert(3, "three"));
{
auto const p1 = c.fetch(3);
auto p2 = std::make_shared<Value>("three");
c.canonicalizeReplaceClient(3, p2);
EXPECT_EQ(p1.get(), p2.get());
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
// Put an object in but keep a strong pointer to it, advance the clock a
// lot, then canonicalize a new object with the same key, make sure you
// get the original object.
{
// Put an object in
EXPECT_FALSE(c.insert(4, "four"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
{
// Keep a strong pointer to it
auto const p1 = c.fetch(4);
EXPECT_NE(p1, nullptr);
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
// Advance the clock a lot
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 1);
// Canonicalize a new object with the same key
auto p2 = std::make_shared<std::string>("four");
EXPECT_TRUE(c.canonicalizeReplaceClient(4, p2));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.getTrackSize(), 1);
// Make sure we get the original object
EXPECT_EQ(p1.get(), p2.get());
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.getTrackSize(), 0);
}
{
EXPECT_FALSE(c.insert(5, "five"));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.size(), 1);
{
auto const p1 = c.fetch(5);
EXPECT_NE(p1, nullptr);
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.size(), 1);
// Advance the clock a lot
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.size(), 1);
auto p2 = std::make_shared<std::string>("five_2");
EXPECT_TRUE(c.canonicalizeReplaceCache(5, p2));
EXPECT_EQ(c.getCacheSize(), 1);
EXPECT_EQ(c.size(), 1);
// Make sure the caller's original pointer is unchanged
EXPECT_NE(p1.get(), p2.get());
EXPECT_EQ(*p2, "five_2");
auto const p3 = c.fetch(5);
EXPECT_NE(p3, nullptr);
EXPECT_EQ(p3.get(), p2.get());
EXPECT_NE(p3.get(), p1.get());
}
++clock;
c.sweep();
EXPECT_EQ(c.getCacheSize(), 0);
EXPECT_EQ(c.size(), 0);
}
{
struct MyRefCountObject : IntrusiveRefCounts
{
std::string data;
// Needed to support weak intrusive pointers
virtual void
partialDestructor()
{
}
MyRefCountObject() = default;
explicit MyRefCountObject(std::string data) : data(std::move(data))
{
}
bool
operator==(std::string const& other) const
{
return data == other;
}
};
using IntrPtrCache = TaggedCache<
Key,
MyRefCountObject,
/*IsKeyCache*/ false,
intr_ptr::SharedWeakUnionPtr<MyRefCountObject>,
intr_ptr::SharedPtr<MyRefCountObject>>;
IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal);
intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared<MyRefCountObject>("one"));
EXPECT_EQ(intrPtrCache.getCacheSize(), 1);
EXPECT_EQ(intrPtrCache.size(), 1);
{
{
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced"));
auto p = intrPtrCache.fetch(1);
EXPECT_EQ(*p, "one_replaced");
// Advance the clock a lot
++clock;
intrPtrCache.sweep();
EXPECT_EQ(intrPtrCache.getCacheSize(), 0);
EXPECT_EQ(intrPtrCache.size(), 1);
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced_2"));
auto p2 = intrPtrCache.fetch(1);
EXPECT_EQ(*p2, "one_replaced_2");
intrPtrCache.del(1, true);
}
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("one_replaced_3"));
auto p3 = intrPtrCache.fetch(1);
EXPECT_EQ(*p3, "one_replaced_3");
}
++clock;
intrPtrCache.sweep();
EXPECT_EQ(intrPtrCache.getCacheSize(), 0);
EXPECT_EQ(intrPtrCache.size(), 0);
}
}
} // namespace xrpl

View File

@@ -0,0 +1,328 @@
#include <xrpl/protocol/Units.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/XRPAmount.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace xrpl::test {
TEST(UnitsTest, types)
{
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
XRPAmount const x{100};
EXPECT_EQ(x.drops(), 100);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
EXPECT_EQ(y.value(), 400);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
auto z = 4 * y;
EXPECT_EQ(z.value(), 1600);
EXPECT_TRUE((std::is_same_v<decltype(z)::unit_type, unit::dropTag>));
FeeLevel32 const f{10};
FeeLevel32 const baseFee{100};
auto drops = mulDiv(baseFee, x, f);
EXPECT_TRUE(drops);
EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(
(std::is_same_v<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
XRPAmount const x{100};
EXPECT_EQ(x.value(), 100);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
EXPECT_EQ(y.value(), 400);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
FeeLevel64 const f{10};
FeeLevel64 const baseFee{100};
auto drops = mulDiv(baseFee, x, f);
EXPECT_TRUE(drops);
EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(
(std::is_same_v<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
FeeLevel64 const x{1024};
EXPECT_EQ(x.value(), 1024);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::feelevelTag>));
std::uint64_t const m = 4;
auto y = m * x;
EXPECT_EQ(y.value(), 4096);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::feelevelTag>));
XRPAmount const basefee{10};
FeeLevel64 const referencefee{256};
auto drops = mulDiv(x, basefee, referencefee);
EXPECT_TRUE(drops);
EXPECT_EQ(drops.value(), 40); // NOLINT(bugprone-unchecked-optional-access)
EXPECT_TRUE(
(std::is_same_v<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
}
TEST(UnitsTest, json)
{
// Json value functionality
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{x.fee()});
}
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{x.fee()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::uint32_t>::max()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{0});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Real);
EXPECT_EQ(y, json::Value{std::numeric_limits<double>::max()});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Real);
EXPECT_EQ(y, json::Value{std::numeric_limits<double>::min()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Int);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::int32_t>::max()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Int);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::int32_t>::min()});
}
}
TEST(UnitsTest, functions)
{
// Explicitly test every defined function for the ValueUnit class
// since some of them are templated, but not used anywhere else.
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
auto make = [&](auto x) -> FeeLevel64 { return x; };
auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; };
[[maybe_unused]]
FeeLevel64 const defaulted{};
FeeLevel64 test{0};
EXPECT_EQ(test.fee(), 0);
test = explicitmake(beast::kZero);
EXPECT_EQ(test.fee(), 0);
test = beast::kZero;
EXPECT_EQ(test.fee(), 0);
test = explicitmake(100u);
EXPECT_EQ(test.fee(), 100);
FeeLevel64 const targetSame{200u};
FeeLevel32 const targetOther{300u};
test = make(targetSame);
EXPECT_EQ(test.fee(), 200);
EXPECT_EQ(test, targetSame);
EXPECT_TRUE(test < FeeLevel64{1000});
EXPECT_TRUE(test > FeeLevel64{100});
test = make(targetOther);
EXPECT_EQ(test.fee(), 300);
EXPECT_EQ(test, targetOther);
test = std::uint64_t(200);
EXPECT_EQ(test.fee(), 200);
test = std::uint32_t(300);
EXPECT_EQ(test.fee(), 300);
test = targetSame;
EXPECT_EQ(test.fee(), 200);
test = targetOther.fee();
EXPECT_EQ(test.fee(), 300);
EXPECT_EQ(test, targetOther);
test = targetSame * 2;
EXPECT_EQ(test.fee(), 400);
test = 3 * targetSame;
EXPECT_EQ(test.fee(), 600);
test = targetSame / 10;
EXPECT_EQ(test.fee(), 20);
test += targetSame;
EXPECT_EQ(test.fee(), 220);
test -= targetSame;
EXPECT_EQ(test.fee(), 20);
test++;
EXPECT_EQ(test.fee(), 21);
++test;
EXPECT_EQ(test.fee(), 22);
test--;
EXPECT_EQ(test.fee(), 21);
--test;
EXPECT_EQ(test.fee(), 20);
test *= 5;
EXPECT_EQ(test.fee(), 100);
test /= 2;
EXPECT_EQ(test.fee(), 50);
test %= 13;
EXPECT_EQ(test.fee(), 11);
/*
// illegal with unsigned
test = -test;
EXPECT_EQ(test.fee(), -11);
EXPECT_EQ(test.signum(), -1);
EXPECT_EQ(to_string(test), "-11");
*/
EXPECT_TRUE(test);
test = 0;
EXPECT_FALSE(test);
EXPECT_EQ(test.signum(), 0);
test = targetSame;
EXPECT_EQ(test.signum(), 1);
EXPECT_EQ(to_string(test), "200");
}
{
auto make = [&](auto x) -> FeeLevelDouble { return x; };
auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; };
[[maybe_unused]]
FeeLevelDouble const defaulted{};
FeeLevelDouble test{0};
EXPECT_EQ(test.fee(), 0);
test = explicitmake(beast::kZero);
EXPECT_EQ(test.fee(), 0);
test = beast::kZero;
EXPECT_EQ(test.fee(), 0);
test = explicitmake(100.0);
EXPECT_EQ(test.fee(), 100);
FeeLevelDouble const targetSame{200.0};
FeeLevel64 const targetOther{300};
test = make(targetSame);
EXPECT_EQ(test.fee(), 200);
EXPECT_EQ(test, targetSame);
EXPECT_TRUE(test < FeeLevelDouble{1000.0});
EXPECT_TRUE(test > FeeLevelDouble{100.0});
test = targetOther.fee();
EXPECT_EQ(test.fee(), 300);
EXPECT_EQ(test, targetOther);
test = 200.0;
EXPECT_EQ(test.fee(), 200);
test = std::uint64_t(300);
EXPECT_EQ(test.fee(), 300);
test = targetSame;
EXPECT_EQ(test.fee(), 200);
test = targetSame * 2;
EXPECT_EQ(test.fee(), 400);
test = 3 * targetSame;
EXPECT_EQ(test.fee(), 600);
test = targetSame / 10;
EXPECT_EQ(test.fee(), 20);
test += targetSame;
EXPECT_EQ(test.fee(), 220);
test -= targetSame;
EXPECT_EQ(test.fee(), 20);
test++;
EXPECT_EQ(test.fee(), 21);
++test;
EXPECT_EQ(test.fee(), 22);
test--;
EXPECT_EQ(test.fee(), 21);
--test;
EXPECT_EQ(test.fee(), 20);
test *= 5;
EXPECT_EQ(test.fee(), 100);
test /= 2;
EXPECT_EQ(test.fee(), 50);
/* illegal with floating
test %= 13;
EXPECT_EQ(test.fee(), 11);
*/
// legal with signed
test = -test;
EXPECT_EQ(test.fee(), -50);
EXPECT_EQ(test.signum(), -1);
EXPECT_EQ(to_string(test), "-50.000000");
EXPECT_TRUE(test);
test = 0;
EXPECT_FALSE(test);
EXPECT_EQ(test.signum(), 0);
test = targetSame;
EXPECT_EQ(test.signum(), 1);
EXPECT_EQ(to_string(test), "200.000000");
}
}
TEST(UnitsTest, initial_xrp)
{
EXPECT_EQ(kInitialXrp.drops(), 100'000'000'000'000'000);
EXPECT_EQ(kInitialXrp, XRPAmount{100'000'000'000'000'000});
}
} // namespace xrpl::test

View File

@@ -0,0 +1,295 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/beast/utility/Zero.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
namespace xrpl {
TEST(XRPAmountTest, sig_num)
{
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
if (i < 0)
{
EXPECT_TRUE(x.signum() < 0);
}
else if (i > 0)
{
EXPECT_TRUE(x.signum() > 0);
}
else
{
EXPECT_EQ(x.signum(), 0);
}
}
}
TEST(XRPAmountTest, beast_zero)
{
using beast::kZero;
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
EXPECT_TRUE((i == 0) == (x == kZero));
EXPECT_TRUE((i != 0) == (x != kZero));
EXPECT_TRUE((i < 0) == (x < kZero));
EXPECT_TRUE((i > 0) == (x > kZero));
EXPECT_TRUE((i <= 0) == (x <= kZero));
EXPECT_TRUE((i >= 0) == (x >= kZero));
EXPECT_TRUE((0 == i) == (kZero == x));
EXPECT_TRUE((0 != i) == (kZero != x));
EXPECT_TRUE((0 < i) == (kZero < x));
EXPECT_TRUE((0 > i) == (kZero > x));
EXPECT_TRUE((0 <= i) == (kZero <= x));
EXPECT_TRUE((0 >= i) == (kZero >= x));
}
}
TEST(XRPAmountTest, comparisons)
{
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
for (auto j : {-1, 0, 1})
{
XRPAmount const y(j);
EXPECT_EQ((i == j), (x == y));
EXPECT_EQ((i != j), (x != y));
EXPECT_EQ((i < j), (x < y));
EXPECT_EQ((i > j), (x > y));
EXPECT_EQ((i <= j), (x <= y));
EXPECT_EQ((i >= j), (x >= y));
}
}
}
TEST(XRPAmountTest, add_sub)
{
for (auto i : {-1, 0, 1})
{
XRPAmount const x(i);
for (auto j : {-1, 0, 1})
{
XRPAmount const y(j);
EXPECT_EQ(XRPAmount(i + j), (x + y));
EXPECT_EQ(XRPAmount(i - j), (x - y));
EXPECT_EQ((x + y), (y + x)); // addition is commutative
}
}
}
TEST(XRPAmountTest, decimal)
{
// Tautology
EXPECT_EQ(kDropsPerXrp.decimalXRP(), 1);
XRPAmount test{1};
EXPECT_EQ(test.decimalXRP(), 0.000001);
test = -test;
EXPECT_EQ(test.decimalXRP(), -0.000001);
test = 100'000'000;
EXPECT_EQ(test.decimalXRP(), 100);
test = -test;
EXPECT_EQ(test.decimalXRP(), -100);
}
TEST(XRPAmountTest, functions)
{
// Explicitly test every defined function for the XRPAmount class
// since some of them are templated, but not used anywhere else.
auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; };
XRPAmount const defaulted{};
(void)defaulted;
XRPAmount test{0};
EXPECT_EQ(test.drops(), 0);
test = make(beast::kZero);
EXPECT_EQ(test.drops(), 0);
test = beast::kZero;
EXPECT_EQ(test.drops(), 0);
test = make(100);
EXPECT_EQ(test.drops(), 100);
test = make(100u);
EXPECT_EQ(test.drops(), 100);
XRPAmount const targetSame{200u};
test = make(targetSame);
EXPECT_EQ(test.drops(), 200);
EXPECT_EQ(test, targetSame);
EXPECT_TRUE(test < XRPAmount{1000});
EXPECT_TRUE(test > XRPAmount{100});
test = std::int64_t(200);
EXPECT_EQ(test.drops(), 200);
test = std::uint32_t(300);
EXPECT_EQ(test.drops(), 300);
test = targetSame;
EXPECT_EQ(test.drops(), 200);
auto testOther = test.dropsAs<std::uint32_t>();
EXPECT_TRUE(testOther);
EXPECT_EQ(*testOther, 200); // NOLINT(bugprone-unchecked-optional-access)
test = std::numeric_limits<std::uint64_t>::max();
testOther = test.dropsAs<std::uint32_t>();
EXPECT_FALSE(testOther);
test = -1;
testOther = test.dropsAs<std::uint32_t>();
EXPECT_FALSE(testOther);
test = targetSame * 2;
EXPECT_EQ(test.drops(), 400);
test = 3 * targetSame;
EXPECT_EQ(test.drops(), 600);
test = 20;
EXPECT_EQ(test.drops(), 20);
test += targetSame;
EXPECT_EQ(test.drops(), 220);
test -= targetSame;
EXPECT_EQ(test.drops(), 20);
test *= 5;
EXPECT_EQ(test.drops(), 100);
test = 50;
EXPECT_EQ(test.drops(), 50);
test -= 39;
EXPECT_EQ(test.drops(), 11);
// legal with signed
test = -test;
EXPECT_EQ(test.drops(), -11);
EXPECT_EQ(test.signum(), -1);
EXPECT_EQ(to_string(test), "-11");
EXPECT_TRUE(test);
test = 0;
EXPECT_FALSE(test);
EXPECT_EQ(test.signum(), 0);
test = targetSame;
EXPECT_EQ(test.signum(), 1);
EXPECT_EQ(to_string(test), "200");
}
TEST(XRPAmountTest, mul_ratio)
{
constexpr auto kMaxUInt32 = std::numeric_limits<std::uint32_t>::max();
constexpr auto kMaxXrp = std::numeric_limits<XRPAmount::value_type>::max();
constexpr auto kMinXrp = std::numeric_limits<XRPAmount::value_type>::min();
{
// multiply by a number that would overflow then divide by the same
// number, and check we didn't lose any value
XRPAmount big(kMaxXrp);
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false));
// multiply and divide by values that would overflow if done
// naively, and check that it gives the correct answer
big -= 0xf; // Subtract a little so it's divisible by 4
EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3);
EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3);
EXPECT_EQ(big.value() % 4, 0);
EXPECT_GT(big.value(), kMaxXrp / 3);
EXPECT_LE(big.value() / 4, kMaxXrp / 3);
}
{
// Similar test as above, but for negative values
XRPAmount big(kMinXrp); // NOLINT TODO
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true));
// rounding mode shouldn't matter as the result is exact
EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false));
// multiply and divide by values that would overflow if done
// naively, and check that it gives the correct answer
EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3);
EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3);
EXPECT_EQ(big.value() % 4, 0);
EXPECT_LT(big.value(), kMinXrp / 3);
EXPECT_GE(big.value() / 4, kMinXrp / 3);
}
{
// small amounts
XRPAmount const tiny(1);
// Round up should give the smallest allowable number
EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt32, true));
// rounding down should be zero
EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt32, false));
EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false));
// tiny negative numbers
XRPAmount const tinyNeg(-1);
// Round up should give zero
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt32, true));
EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true));
// rounding down should be tiny
EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false));
}
{ // rounding
{
XRPAmount const one(1);
auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false);
EXPECT_EQ(rup.drops() - rdown.drops(), 1);
}
{
XRPAmount const big(kMaxXrp);
auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false);
EXPECT_EQ(rup.drops() - rdown.drops(), 1);
}
{
XRPAmount const negOne(-1);
auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true);
auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false);
EXPECT_EQ(rup.drops() - rdown.drops(), 1);
}
}
{
// division by zero
XRPAmount const one(1);
EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); });
}
{
// overflow
XRPAmount const big(kMaxXrp);
EXPECT_ANY_THROW({ mulRatio(big, 2, 1, true); });
}
{
// underflow
XRPAmount const bigNegative(kMinXrp + 10);
EXPECT_EQ(mulRatio(bigNegative, 2, 1, true), kMinXrp);
}
}
} // namespace xrpl

View File

@@ -0,0 +1,438 @@
#include <xrpl/protocol/detail/token_errors.h>
#include <boost/multiprecision/cpp_int.hpp> // IWYU pragma: keep
#include <gtest/gtest.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <ranges>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
#ifndef _MSC_VER
#include <xrpl/protocol/detail/b58_utils.h>
#include <xrpl/protocol/tokens.h>
#include <array>
#include <cstddef>
#include <random>
#include <span>
#include <sstream>
namespace xrpl::test {
namespace {
[[nodiscard]] inline auto
randEngine() -> std::mt19937&
{
static std::mt19937 kR = [] {
std::random_device rd;
return std::mt19937{rd()};
}();
return kR;
}
constexpr int kNumTokenTypeIndexes = 9;
[[nodiscard]] inline auto
tokenTypeAndSize(int i) -> std::tuple<xrpl::TokenType, std::size_t>
{
assert(i < kNumTokenTypeIndexes);
switch (i)
{
using enum xrpl::TokenType;
case 0:
return {None, 20};
case 1:
return {NodePublic, 32};
case 2:
return {NodePublic, 33};
case 3:
return {NodePrivate, 32};
case 4:
return {AccountID, 20};
case 5:
return {AccountPublic, 32};
case 6:
return {AccountPublic, 33};
case 7:
return {AccountSecret, 32};
case 8:
return {FamilySeed, 16};
default:
throw std::invalid_argument(
"Invalid token selection passed to tokenTypeAndSize() "
"in " __FILE__);
}
}
[[nodiscard]] inline auto
randomTokenTypeAndSize() -> std::tuple<xrpl::TokenType, std::size_t>
{
using namespace xrpl;
auto& rng = randEngine();
std::uniform_int_distribution<> d(0, 8);
return tokenTypeAndSize(d(rng));
}
// Return the token type and subspan of `d` to use as test data.
[[nodiscard]] inline auto
randomB256TestData(std::span<std::uint8_t> d)
-> std::tuple<xrpl::TokenType, std::span<std::uint8_t>>
{
auto& rng = randEngine();
std::uniform_int_distribution<std::uint8_t> dist(0, 255);
auto [tokType, tokSize] = randomTokenTypeAndSize();
std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); });
return {tokType, d.subspan(0, tokSize)};
}
inline void
printAsChar(std::span<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> s) {
std::string r;
r.resize(s.size());
std::ranges::copy(s, r.begin());
return r;
};
auto sa = asString(a);
auto sb = asString(b);
std::cerr << "\n\n" << sa << "\n" << sb << "\n";
}
inline void
printAsInt(std::span<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> s) -> std::string {
std::stringstream sstr;
for (auto i : s)
{
sstr << std::setw(3) << int(i) << ',';
}
return sstr.str();
};
auto sa = asString(a);
auto sb = asString(b);
std::cerr << "\n\n" << sa << "\n" << sb << "\n";
}
} // namespace
namespace multiprecision_utils {
boost::multiprecision::checked_uint512_t
toBoostMP(std::span<std::uint64_t> in)
{
boost::multiprecision::checked_uint512_t mbp = 0;
for (auto const& i : std::views::reverse(in))
{
mbp <<= 64;
mbp += i;
}
return mbp;
}
std::vector<std::uint64_t>
randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5)
{
auto eng = randEngine();
std::uniform_int_distribution<std::uint8_t> numCoeffDist(minSize, maxSize);
std::uniform_int_distribution<std::uint64_t> dist;
auto const numCoeff = numCoeffDist(eng);
std::vector<std::uint64_t> coeffs;
coeffs.reserve(numCoeff);
for (int i = 0; i < numCoeff; ++i)
{
coeffs.push_back(dist(eng));
}
return coeffs;
}
} // namespace multiprecision_utils
TEST(Base58Test, multiprecision)
{
using namespace boost::multiprecision;
constexpr std::size_t kIters = 100000;
auto eng = randEngine();
std::uniform_int_distribution<std::uint64_t> dist;
std::uniform_int_distribution<std::uint64_t> dist1(1);
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
if (d == 0u)
continue;
auto bigInt = multiprecision_utils::randomBigInt();
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refDiv = boostBigInt / d;
auto const refMod = boostBigInt % d;
auto const mod = b58_fast::detail::inplaceBigintDivRem(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
auto const foundDiv = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refMod.convert_to<std::uint64_t>(), mod);
EXPECT_EQ(foundDiv, refDiv);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2);
if (bigInt[bigInt.size() - 1] == std::numeric_limits<std::uint64_t>::max())
{
bigInt[bigInt.size() - 1] -= 1; // Prevent overflow
}
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::Success);
auto const foundAdd = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refAdd, foundAdd);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist1(eng);
// Force overflow
std::vector<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::OverflowAdd);
auto const foundAdd = multiprecision_utils::toBoostMP(bigInt);
EXPECT_NE(refAdd, foundAdd);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist(eng);
auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2);
// inplace mul requires the most significant coeff to be zero to
// hold the result.
bigInt[bigInt.size() - 1] = 0;
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::Success);
auto const foundMul = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refMul, foundMul);
}
for (int i = 0; i < kIters; ++i)
{
std::uint64_t const d = dist1(eng);
// Force overflow
std::vector<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
EXPECT_EQ(result, TokenCodecErrc::InputTooLarge);
auto const foundMul = multiprecision_utils::toBoostMP(bigInt);
EXPECT_NE(refMul, foundMul);
}
}
TEST(Base58Test, fast_matches_ref)
{
auto testRawEncode = [&](std::span<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b256Result;
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b58ResultBuf[i]};
if (i == 0)
{
auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf);
EXPECT_TRUE(r);
b58Result[i] = r.value();
}
else
{
std::array<std::uint8_t, 128> tmpBuf{};
std::string const s = xrpl::b58_ref::detail::encodeBase58(
b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size());
EXPECT_TRUE(s.size());
b58Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b58Result[i].begin());
}
}
auto const rawB58SameSize = b58Result[0].size() == b58Result[1].size();
EXPECT_TRUE(rawB58SameSize);
if (rawB58SameSize)
{
auto const rawB58SameData =
memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0;
EXPECT_TRUE(rawB58SameData);
if (!rawB58SameData)
{
printAsChar(b58Result[0], b58Result[1]);
}
}
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()};
if (i == 0)
{
std::string const in(
b58Result[i].data(), b58Result[i].data() + b58Result[i].size());
auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf);
EXPECT_TRUE(r);
b256Result[i] = r.value();
}
else
{
std::string const st(b58Result[i].begin(), b58Result[i].end());
std::string const s = xrpl::b58_ref::detail::decodeBase58(st);
EXPECT_TRUE(s.size());
b256Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b256Result[i].begin());
}
}
auto const rawB256SameSize = b256Result[0].size() == b256Result[1].size();
EXPECT_TRUE(rawB256SameSize);
if (rawB256SameSize)
{
auto const rawB256SameData =
memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0;
EXPECT_TRUE(rawB256SameData);
if (!rawB256SameData)
{
printAsInt(b256Result[0], b256Result[1]);
}
}
};
auto testTokenEncode = [&](xrpl::TokenType const tokType,
std::span<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b256Result;
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()};
if (i == 0)
{
auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf);
EXPECT_TRUE(r);
b58Result[i] = r.value();
}
else
{
std::string const s =
xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size());
EXPECT_TRUE(s.size());
b58Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b58Result[i].begin());
}
}
auto const tokenB58SameSize = b58Result[0].size() == b58Result[1].size();
EXPECT_TRUE(tokenB58SameSize);
if (tokenB58SameSize)
{
auto const tokenB58SameData =
memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0;
EXPECT_TRUE(tokenB58SameData);
if (!tokenB58SameData)
{
printAsChar(b58Result[0], b58Result[1]);
}
}
for (int i = 0; i < 2; ++i)
{
std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()};
if (i == 0)
{
std::string const in(
b58Result[i].data(), b58Result[i].data() + b58Result[i].size());
auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf);
EXPECT_TRUE(r);
b256Result[i] = r.value();
}
else
{
std::string const st(b58Result[i].begin(), b58Result[i].end());
std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType);
EXPECT_TRUE(s.size());
b256Result[i] = outBuf.subspan(0, s.size());
std::ranges::copy(s, b256Result[i].begin());
}
}
auto const tokenB256SameSize = b256Result[0].size() == b256Result[1].size();
EXPECT_TRUE(tokenB256SameSize);
if (tokenB256SameSize)
{
auto const tokenB256SameData =
memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0;
EXPECT_TRUE(tokenB256SameData);
if (!tokenB256SameData)
{
printAsInt(b256Result[0], b256Result[1]);
}
}
};
auto testIt = [&](xrpl::TokenType const tokType, std::span<std::uint8_t> const& b256Data) {
testRawEncode(b256Data);
testTokenEncode(tokType, b256Data);
};
// test every token type with data where every byte is the same and the
// bytes range from 0-255
for (int i = 0; i < kNumTokenTypeIndexes; ++i)
{
std::array<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, tokSize] = tokenTypeAndSize(i);
for (int d = 0; d <= 255; ++d)
{
memset(b256DataBuf.data(), d, tokSize);
testIt(tokType, std::span(b256DataBuf.data(), tokSize));
}
}
// test with random data
constexpr std::size_t kIters = 100000;
for (int i = 0; i < kIters; ++i)
{
std::array<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, b256Data] = randomB256TestData(b256DataBuf);
testIt(tokType, b256Data);
}
}
} // namespace xrpl::test
#endif // _MSC_VER

View File

@@ -0,0 +1,360 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/utility/Zero.h>
#include <boost/endian/detail/order.hpp>
#include <gtest/gtest.h>
#include <array>
#include <cassert>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl::test {
// a non-hashing Hasher that just copies the bytes.
// Used to test hash_append in base_uint
template <std::size_t Bits>
struct Nonhash
{
static constexpr auto const kEndian = boost::endian::order::big;
static constexpr std::size_t kWidth = Bits / 8;
std::array<std::uint8_t, kWidth> data;
Nonhash() = default;
void
operator()(void const* key, std::size_t len) noexcept
{
assert(len == kWidth);
memcpy(data.data(), key, len);
}
explicit
operator std::size_t() noexcept
{
return kWidth;
}
};
struct BaseUintTest : public ::testing::Test
{
using BaseUInt96 = BaseUInt<96>;
static_assert(std::is_copy_constructible_v<BaseUInt96>);
static_assert(std::is_copy_assignable_v<BaseUInt96>);
static void
testComparisons()
{
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 6> kTestArgs{
{{"0000000000000000", "0000000000000001"},
{"0000000000000000", "ffffffffffffffff"},
{"1234567812345678", "2345678923456789"},
{"8000000000000000", "8000000000000001"},
{"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"},
{"fffffffffffffffe", "ffffffffffffffff"}}};
for (auto const& arg : kTestArgs)
{
xrpl::BaseUInt<64> const u{arg.first}, v{arg.second};
// For code readability, we want to use general boolean
// expectations instead of specific EXPECT_LT etc.
EXPECT_TRUE(u < v);
EXPECT_TRUE(u <= v);
EXPECT_TRUE(u != v);
EXPECT_FALSE(u == v);
EXPECT_FALSE(u > v);
EXPECT_FALSE(u >= v);
EXPECT_FALSE(v < u);
EXPECT_FALSE(v <= u);
EXPECT_TRUE(v != u);
EXPECT_FALSE(v == u);
EXPECT_TRUE(v > u);
EXPECT_TRUE(v >= u);
EXPECT_TRUE(u == u);
EXPECT_TRUE(v == v);
}
}
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 6> kTestArgs{
{
{"000000000000000000000000", "000000000000000000000001"},
{"000000000000000000000000", "ffffffffffffffffffffffff"},
{"0123456789ab0123456789ab", "123456789abc123456789abc"},
{"555555555555555555555555", "55555555555a555555555555"},
{"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"},
{"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"},
}};
for (auto const& arg : kTestArgs)
{
xrpl::BaseUInt<96> const u{arg.first}, v{arg.second};
EXPECT_TRUE(u < v);
EXPECT_TRUE(u <= v);
EXPECT_TRUE(u != v);
EXPECT_FALSE(u == v);
EXPECT_FALSE(u > v);
EXPECT_FALSE(u >= v);
EXPECT_FALSE(v < u);
EXPECT_FALSE(v <= u);
EXPECT_TRUE(v != u);
EXPECT_FALSE(v == u);
EXPECT_TRUE(v > u);
EXPECT_TRUE(v >= u);
EXPECT_TRUE(u == u);
EXPECT_TRUE(v == v);
}
}
}
};
TEST_F(BaseUintTest, base_uint)
{
static_assert(!std::is_constructible_v<BaseUInt96, std::complex<double>>);
static_assert(!std::is_assignable_v<BaseUInt96&, std::complex<double>>);
testComparisons();
// used to verify set insertion (hashing required)
std::unordered_set<BaseUInt96, HardenedHash<>> uset;
Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
EXPECT_EQ(BaseUInt96::kBytes, raw.size());
BaseUInt96 u = BaseUInt96::fromRaw(raw);
uset.insert(u);
EXPECT_EQ(raw.size(), u.size());
EXPECT_EQ(to_string(u), "0102030405060708090A0B0C");
EXPECT_EQ(toShortString(u), "01020304...");
EXPECT_EQ(*u.data(), 1);
EXPECT_EQ(u.signum(), 1);
EXPECT_FALSE(!u);
EXPECT_FALSE(u.isZero());
EXPECT_TRUE(u.isNonZero());
unsigned char t = 0;
for (auto& d : u)
{
EXPECT_EQ(d, ++t);
}
// Test hash_append by "hashing" with a no-op hasher (h)
// and then extracting the bytes that were written during hashing
// back into another base_uint (w) for comparison with the original
Nonhash<96> h{};
hash_append(h, u);
BaseUInt96 const w =
BaseUInt96::fromRaw(std::vector<std::uint8_t>(h.data.begin(), h.data.end()));
EXPECT_EQ(w, u);
BaseUInt96 v{~u};
uset.insert(v);
EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3");
EXPECT_EQ(toShortString(v), "FEFDFCFB...");
EXPECT_EQ(*v.data(), 0xfe);
EXPECT_EQ(v.signum(), 1);
EXPECT_FALSE(!v);
EXPECT_FALSE(v.isZero());
EXPECT_TRUE(v.isNonZero());
t = 0xff;
for (auto& d : v)
{
EXPECT_EQ(d, --t);
}
EXPECT_LT(u, v);
EXPECT_GT(v, u);
v = u;
EXPECT_EQ(v, u);
BaseUInt96 z{beast::kZero};
uset.insert(z);
EXPECT_EQ(to_string(z), "000000000000000000000000");
EXPECT_EQ(toShortString(z), "00000000...");
EXPECT_EQ(*z.data(), 0);
EXPECT_EQ(*z.begin(), 0);
EXPECT_EQ(*std::prev(z.end(), 1), 0);
EXPECT_EQ(z.signum(), 0);
EXPECT_TRUE(!z);
EXPECT_TRUE(z.isZero());
EXPECT_FALSE(z.isNonZero());
for (auto& d : z)
{
EXPECT_EQ(d, 0);
}
BaseUInt96 n{z};
n++;
EXPECT_EQ(n, BaseUInt96(1));
n--;
EXPECT_EQ(n, beast::kZero);
EXPECT_EQ(n, z);
n--;
EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF");
EXPECT_EQ(toShortString(n), "FFFFFFFF...");
n = beast::kZero;
EXPECT_EQ(n, z);
BaseUInt96 zp1{z};
zp1++;
BaseUInt96 zm1{z};
zm1--;
BaseUInt96 const x{zm1 ^ zp1};
uset.insert(x);
EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x);
EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x);
EXPECT_EQ(uset.size(), 4);
BaseUInt96 tmp;
EXPECT_TRUE(tmp.parseHex(to_string(u)));
EXPECT_EQ(tmp, u);
tmp = z;
// fails with extra char
EXPECT_FALSE(tmp.parseHex("A" + to_string(u)));
tmp = z;
// fails with extra char at end
EXPECT_FALSE(tmp.parseHex(to_string(u) + "A"));
// fails with a non-hex character at some point in the string:
tmp = z;
for (std::size_t i = 0; i != 24; ++i)
{
std::string x = to_string(z);
x[i] = ('G' + (i % 10));
EXPECT_FALSE(tmp.parseHex(x));
}
// Walking 1s:
for (std::size_t i = 0; i != 24; ++i)
{
std::string s1 = "000000000000000000000000";
s1[i] = '1';
EXPECT_TRUE(tmp.parseHex(s1));
EXPECT_EQ(to_string(tmp), s1);
}
// Walking 0s:
for (std::size_t i = 0; i != 24; ++i)
{
std::string s1 = "111111111111111111111111";
s1[i] = '0';
EXPECT_TRUE(tmp.parseHex(s1));
EXPECT_EQ(to_string(tmp), s1);
}
// Constexpr constructors
{
static_assert(BaseUInt96{}.signum() == 0);
static_assert(BaseUInt96("0").signum() == 0);
static_assert(BaseUInt96("000000000000000000000000").signum() == 0);
static_assert(BaseUInt96("000000000000000000000001").signum() == 1);
static_assert(BaseUInt96("800000000000000000000000").signum() == 1);
// Everything within the #if should fail during compilation.
#if 0
// Too few characters
static_assert(BaseUInt96("00000000000000000000000").signum() == 0);
// Too many characters
static_assert(BaseUInt96("0000000000000000000000000").signum() == 0);
// Non-hex characters
static_assert(BaseUInt96("00000000000000000000000 ").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000/").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000:").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000@").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000G").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000`").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000g").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000~").signum() == 1);
#endif // 0
// Using the constexpr constructor in a non-constexpr context
// with an error in the parsing throws an exception.
{
// Invalid length for string.
bool caught = false;
try
{
// Try to prevent constant evaluation.
std::vector<char> str(23, '7');
std::string_view const sView(str.data(), str.size());
[[maybe_unused]] BaseUInt96 const t96(sView);
}
catch (std::invalid_argument const& e)
{
EXPECT_EQ(e.what(), std::string("invalid length for hex string"));
caught = true;
}
EXPECT_TRUE(caught);
}
{
// Invalid character in string.
bool caught = false;
try
{
// Try to prevent constant evaluation.
std::vector<char> str(23, '7');
str.push_back('G');
std::string_view const sView(str.data(), str.size());
[[maybe_unused]] BaseUInt96 const t96(sView);
}
catch (std::range_error const& e)
{
EXPECT_EQ(e.what(), std::string("invalid hex character"));
caught = true;
}
EXPECT_TRUE(caught);
}
// Verify that constexpr base_uints interpret a string the same
// way parseHex() does.
struct StrBaseUInt
{
char const* const str;
BaseUInt96 tst;
constexpr StrBaseUInt(char const* s) : str(s), tst(s)
{
}
};
constexpr StrBaseUInt kTestCases[] = {
"000000000000000000000000",
"000000000000000000000001",
"fedcba9876543210ABCDEF91",
"19FEDCBA0123456789abcdef",
"800000000000000000000000",
"fFfFfFfFfFfFfFfFfFfFfFfF",
};
for (StrBaseUInt const& t : kTestCases)
{
BaseUInt96 t96;
EXPECT_TRUE(t96.parseHex(t.str));
EXPECT_EQ(t96, t.tst);
}
}
}
} // namespace xrpl::test

View File

@@ -1,6 +1,8 @@
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/hash/hash_append.h>
#include <xrpl/beast/unit_test/suite.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
@@ -153,20 +155,20 @@ static_assert(sha256_t::kBits == 256, "sha256_t must have 256 bits");
namespace xrpl {
class hardened_hash_test : public beast::unit_test::Suite
class HardenedHashTest : public ::testing::Test
{
public:
template <class T>
void
static void
check()
{
T t{};
HardenedHash<>()(t);
pass();
SUCCEED();
}
template <template <class T> class U>
void
static void
checkUserType()
{
check<U<bool>>();
@@ -191,48 +193,35 @@ public:
}
template <template <class T> class C>
void
static void
checkContainer()
{
{
C<detail::TestUserTypeMember<std::string>> const c;
}
pass();
SUCCEED();
{
C<detail::TestUserTypeFree<std::string>> const c;
}
pass();
}
void
testUserTypes()
{
testcase("user types");
checkUserType<detail::TestUserTypeMember>();
checkUserType<detail::TestUserTypeFree>();
}
void
testContainers()
{
testcase("containers");
checkContainer<detail::test_hardened_unordered_set>();
checkContainer<detail::test_hardened_unordered_map>();
checkContainer<detail::test_hardened_unordered_multiset>();
checkContainer<detail::test_hardened_unordered_multimap>();
}
void
run() override
{
testUserTypes();
testContainers();
SUCCEED();
}
};
BEAST_DEFINE_TESTSUITE(hardened_hash, basics, xrpl);
TEST_F(HardenedHashTest, user_types)
{
checkUserType<detail::TestUserTypeMember>();
checkUserType<detail::TestUserTypeFree>();
}
TEST_F(HardenedHashTest, containers)
{
checkContainer<detail::test_hardened_unordered_set>();
checkContainer<detail::test_hardened_unordered_map>();
checkContainer<detail::test_hardened_unordered_multiset>();
checkContainer<detail::test_hardened_unordered_multimap>();
}
} // namespace xrpl

View File

@@ -0,0 +1,80 @@
#include <xrpl/basics/join.h>
#include <xrpl/basics/base_uint.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <initializer_list>
#include <sstream>
#include <string>
#include <vector>
namespace xrpl::test {
struct JoinTest : public ::testing::Test
{
};
TEST_F(JoinTest, join)
{
auto test = [](auto collectionanddelimiter, std::string expected) {
std::stringstream ss;
// Put something else in the buffer before and after to ensure that
// the << operator returns the stream correctly.
ss << "(" << collectionanddelimiter << ")";
auto const str = ss.str();
EXPECT_EQ(str.substr(1, str.length() - 2), expected);
EXPECT_EQ(str.front(), '(');
EXPECT_EQ(str.back(), ')');
};
// C++ array
test(CollectionAndDelimiter(std::array<int, 4>{2, -1, 5, 10}, "/"), "2/-1/5/10");
// One item C++ array edge case
test(CollectionAndDelimiter(std::array<std::string, 1>{"test"}, " & "), "test");
// Empty C++ array edge case
test(CollectionAndDelimiter(std::array<int, 0>{}, ","), "");
{
// C-style array
char letters[4]{'w', 'a', 's', 'd'};
test(CollectionAndDelimiter(letters, std::to_string(0)), "w0a0s0d");
}
{
// Auto sized C-style array
std::string words[]{"one", "two", "three", "four"};
test(CollectionAndDelimiter(words, "\n"), "one\ntwo\nthree\nfour");
}
{
// One item C-style array edge case
std::string words[]{"thing"};
test(CollectionAndDelimiter(words, "\n"), "thing");
}
// Initializer list
test(CollectionAndDelimiter(std::initializer_list<size_t>{19, 25}, "+"), "19+25");
// vector
test(CollectionAndDelimiter(std::vector<int>{0, 42}, std::to_string(99)), "09942");
// vector with one item edge case
test(CollectionAndDelimiter(std::vector<std::string>{"master"}, "xxx"), "master");
// vector with one non-trivial streamable item edge case
test(
CollectionAndDelimiter(std::vector<uint256>{uint256{1}}, "xxx"),
"0000000000000000000000000000000000000000000000000000000000000001");
// empty vector edge case
test(CollectionAndDelimiter(std::vector<uint256>{}, ","), "");
// C-style string
test(CollectionAndDelimiter("string", " "), "s t r i n g");
// Empty C-style string edge case
test(CollectionAndDelimiter("", "*"), "");
// Single char C-style string edge case
test(CollectionAndDelimiter("x", "*"), "x");
// std::string
test(CollectionAndDelimiter(std::string{"string"}, "-"), "s-t-r-i-n-g");
// Empty std::string edge case
test(CollectionAndDelimiter(std::string{""}, "*"), "");
// Single char std::string edge case
test(CollectionAndDelimiter(std::string{"y"}, "*"), "y");
}
} // namespace xrpl::test

View File

@@ -0,0 +1,241 @@
#include <xrpl/resource/detail/Logic.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/net/IPAddressV4.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/resource/Disposition.h>
#include <xrpl/resource/Gossip.h>
#include <xrpl/resource/detail/Tuning.h>
#include <boost/utility/base_from_member.hpp>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <chrono>
#include <cstdint>
#include <string>
namespace xrpl::Resource {
class ResourceManagerTest : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
class TestLogic : private boost::base_from_member<TestStopwatch>, public Logic
{
private:
using clock_type = boost::base_from_member<TestStopwatch>;
public:
explicit TestLogic(beast::Journal journal)
: Logic(beast::insight::NullCollector::make(), member, journal)
{
}
void
advance()
{
++member;
}
TestStopwatch&
clock()
{
return member;
}
};
//--------------------------------------------------------------------------
static void
populateGossip(Gossip& gossip)
{
std::uint8_t const v(10 + randInt(9));
std::uint8_t const n(10 + randInt(9));
gossip.items.reserve(n);
for (std::uint8_t i = 0; i < n; ++i)
{
Gossip::Item item;
item.balance = 100 + randInt(499);
beast::IP::AddressV4::bytes_type const d = {{
192,
0,
2,
static_cast<std::uint8_t>(v + i),
}};
item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}};
gossip.items.push_back(item);
}
}
};
TEST_F(ResourceManagerTest, limited_warn_drop)
{
TestLogic logic{j_};
Charge const fee{kDropThreshold + 1};
beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")};
{
Consumer c{logic.newInboundEndpoint(addr)};
// Create load until we get a warning
int n = 10000;
bool warned = false;
while (--n >= 0)
{
if (c.charge(fee) == Disposition::Warn)
{
warned = true;
break;
}
++logic.clock();
}
ASSERT_TRUE(warned) << "Loop count exceeded without warning";
// Create load until we get dropped
bool dropped = false;
while (--n >= 0)
{
if (c.charge(fee) == Disposition::Drop)
{
dropped = true;
// Disconnect abusive Consumer
EXPECT_TRUE(c.disconnect(j_));
break;
}
++logic.clock();
}
ASSERT_TRUE(dropped) << "Loop count exceeded without dropping";
}
// Make sure the consumer is on the blacklist for a while.
{
Consumer const c{logic.newInboundEndpoint(addr)};
logic.periodicActivity();
EXPECT_EQ(c.disposition(), Disposition::Drop) << "Dropped consumer not put on blacklist";
}
// Makes sure the Consumer is eventually removed from blacklist
bool readmitted = false;
{
using namespace std::chrono_literals;
// Give Consumer time to become readmitted. Should never
// exceed expiration time.
auto n = kSecondsUntilExpiration + 1s;
while (--n > 0s)
{
++logic.clock();
logic.periodicActivity();
Consumer const c{logic.newInboundEndpoint(addr)};
if (c.disposition() != Disposition::Drop)
{
readmitted = true;
break;
}
}
}
EXPECT_TRUE(readmitted) << "Dropped Consumer left on blacklist too long";
}
TEST_F(ResourceManagerTest, unlimited_warn_drop)
{
TestLogic logic{j_};
Charge const fee{kDropThreshold + 1};
beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")};
Consumer c{logic.newUnlimitedEndpoint(addr)};
// Create load until we get a warning
int n = 10000;
bool warned = false;
while (--n >= 0)
{
if (c.charge(fee) == Disposition::Warn)
{
warned = true;
break;
}
++logic.clock();
}
EXPECT_FALSE(warned) << "Should loop forever with no warning";
}
TEST_F(ResourceManagerTest, charges)
{
TestLogic logic{j_};
{
beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.1")};
Consumer c{logic.newInboundEndpoint(address)};
Charge const fee{1000};
JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second";
c.charge(fee);
for (int i = 0; i < 128; ++i)
{
JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count()
<< ", Balance = " << c.balance();
logic.advance();
}
}
{
beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.2")};
Consumer c{logic.newInboundEndpoint(address)};
Charge const fee{1000};
JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second";
for (int i = 0; i < 128; ++i)
{
c.charge(fee);
JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count()
<< ", Balance = " << c.balance();
logic.advance();
}
}
}
TEST_F(ResourceManagerTest, imports)
{
TestLogic logic{j_};
Gossip g[5];
for (auto& i : g)
populateGossip(i);
for (int i = 0; i < 5; ++i)
logic.importConsumers(std::to_string(i), g[i]);
}
TEST_F(ResourceManagerTest, import)
{
TestLogic logic{j_};
Gossip g;
Gossip::Item item;
item.balance = 100;
beast::IP::AddressV4::bytes_type const d = {{
192,
0,
2,
1,
}};
item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}};
g.items.push_back(item);
logic.importConsumers("g", g);
}
} // namespace xrpl::Resource

View File

@@ -0,0 +1,22 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <memory>
namespace xrpl::tests {
TEST(FetchPackTest, construct_table)
{
beast::Journal const j{TestSink::instance()};
TestNodeFamily f{j};
std::shared_ptr<SHAMap> const t1{std::make_shared<SHAMap>(SHAMapType::FREE, f)};
EXPECT_NE(t1, nullptr);
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,349 @@
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/shamap/SHAMapInnerNode.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapLeafNode.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl::tests {
#ifndef __INTELLISENSE__
static_assert(std::is_nothrow_destructible<SHAMap>{});
static_assert(!std::is_default_constructible<SHAMap>{});
static_assert(!std::is_copy_constructible<SHAMap>{});
static_assert(!std::is_copy_assignable<SHAMap>{});
static_assert(!std::is_move_constructible<SHAMap>{});
static_assert(!std::is_move_assignable<SHAMap>{});
static_assert(std::is_nothrow_destructible<SHAMap::ConstIterator>{});
static_assert(std::is_copy_constructible<SHAMap::ConstIterator>{});
static_assert(std::is_copy_assignable<SHAMap::ConstIterator>{});
static_assert(std::is_move_constructible<SHAMap::ConstIterator>{});
static_assert(std::is_move_assignable<SHAMap::ConstIterator>{});
static_assert(std::is_nothrow_destructible<SHAMapItem>{});
static_assert(!std::is_default_constructible<SHAMapItem>{});
static_assert(!std::is_copy_constructible<SHAMapItem>{});
static_assert(std::is_nothrow_destructible<SHAMapNodeID>{});
static_assert(std::is_default_constructible<SHAMapNodeID>{});
static_assert(std::is_copy_constructible<SHAMapNodeID>{});
static_assert(std::is_copy_assignable<SHAMapNodeID>{});
static_assert(std::is_move_constructible<SHAMapNodeID>{});
static_assert(std::is_move_assignable<SHAMapNodeID>{});
static_assert(std::is_nothrow_destructible<SHAMapHash>{});
static_assert(std::is_default_constructible<SHAMapHash>{});
static_assert(std::is_copy_constructible<SHAMapHash>{});
static_assert(std::is_copy_assignable<SHAMapHash>{});
static_assert(std::is_move_constructible<SHAMapHash>{});
static_assert(std::is_move_assignable<SHAMapHash>{});
static_assert(std::is_nothrow_destructible<SHAMapTreeNode>{});
static_assert(!std::is_default_constructible<SHAMapTreeNode>{});
static_assert(!std::is_copy_constructible<SHAMapTreeNode>{});
static_assert(!std::is_copy_assignable<SHAMapTreeNode>{});
static_assert(!std::is_move_constructible<SHAMapTreeNode>{});
static_assert(!std::is_move_assignable<SHAMapTreeNode>{});
static_assert(std::is_nothrow_destructible<SHAMapInnerNode>{});
static_assert(!std::is_default_constructible<SHAMapInnerNode>{});
static_assert(!std::is_copy_constructible<SHAMapInnerNode>{});
static_assert(!std::is_copy_assignable<SHAMapInnerNode>{});
static_assert(!std::is_move_constructible<SHAMapInnerNode>{});
static_assert(!std::is_move_assignable<SHAMapInnerNode>{});
static_assert(std::is_nothrow_destructible<SHAMapLeafNode>{});
static_assert(!std::is_default_constructible<SHAMapLeafNode>{});
static_assert(!std::is_copy_constructible<SHAMapLeafNode>{});
static_assert(!std::is_copy_assignable<SHAMapLeafNode>{});
static_assert(!std::is_move_constructible<SHAMapLeafNode>{});
static_assert(!std::is_move_assignable<SHAMapLeafNode>{});
#endif
inline bool
operator!=(SHAMapItem const& a, SHAMapItem const& b)
{
return a.key() != b.key();
}
struct SHAMapBackingMode
{
bool backed;
std::string_view testName;
};
constexpr SHAMapBackingMode kBackedMode{.backed = true, .testName = "backed"};
constexpr SHAMapBackingMode kUnbackedMode{.backed = false, .testName = "unbacked"};
std::string
shamapBackingModeName(::testing::TestParamInfo<SHAMapBackingMode> const& info)
{
return std::string{info.param.testName};
}
class SHAMapTest : public ::testing::TestWithParam<SHAMapBackingMode>
{
protected:
beast::Journal const j_{TestSink::instance()};
static Buffer
intToVuc(std::uint8_t v)
{
Buffer vuc{32};
std::fill_n(vuc.data(), vuc.size(), v);
return vuc;
}
};
TEST_P(SHAMapTest, add_traverse_snapshot_build_tear_and_iterate)
{
auto const testMode = GetParam();
tests::TestNodeFamily f{j_};
// kH3 and kH4 differ only in the leaf, same terminal node (level 19)
constexpr uint256 kH1("092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 kH2("436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 kH3("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 kH4("b92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
SHAMap sMap{SHAMapType::FREE, f};
sMap.invariants();
if (!testMode.backed)
sMap.setUnbacked();
auto i1 = makeShamapitem(kH1, intToVuc(1));
auto i2 = makeShamapitem(kH2, intToVuc(2));
auto i3 = makeShamapitem(kH3, intToVuc(3));
auto i4 = makeShamapitem(kH4, intToVuc(4));
EXPECT_TRUE(sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i2))) << "no add";
sMap.invariants();
EXPECT_TRUE(sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i1))) << "no add";
sMap.invariants();
auto i = sMap.begin();
auto e = sMap.end();
EXPECT_FALSE(i == e || (*i != *i1)) << "bad traverse";
++i;
EXPECT_FALSE(i == e || (*i != *i2)) << "bad traverse";
++i;
EXPECT_EQ(i, e) << "bad traverse";
sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i4));
sMap.invariants();
sMap.delItem(i2->key());
sMap.invariants();
sMap.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(*i3));
sMap.invariants();
i = sMap.begin();
e = sMap.end();
EXPECT_FALSE(i == e || (*i != *i1)) << "bad traverse";
++i;
EXPECT_FALSE(i == e || (*i != *i3)) << "bad traverse";
++i;
EXPECT_FALSE(i == e || (*i != *i4)) << "bad traverse";
++i;
EXPECT_EQ(i, e) << "bad traverse";
SHAMapHash const mapHash = sMap.getHash();
std::shared_ptr<SHAMap> const map2 = sMap.snapShot(false);
map2->invariants();
EXPECT_EQ(sMap.getHash(), mapHash) << "bad snapshot";
EXPECT_EQ(map2->getHash(), mapHash) << "bad snapshot";
SHAMap::Delta delta;
ASSERT_TRUE(sMap.compare(*map2, delta, 100));
EXPECT_TRUE(delta.empty());
EXPECT_TRUE(sMap.delItem(sMap.begin()->key())) << "bad mod";
sMap.invariants();
EXPECT_NE(sMap.getHash(), mapHash) << "bad snapshot";
EXPECT_EQ(map2->getHash(), mapHash) << "bad snapshot";
ASSERT_TRUE(sMap.compare(*map2, delta, 100));
ASSERT_EQ(delta.size(), 1);
EXPECT_EQ(delta.begin()->first, kH1);
EXPECT_EQ(delta.begin()->second.first, nullptr);
ASSERT_NE(delta.begin()->second.second, nullptr);
EXPECT_EQ(delta.begin()->second.second->key(), kH1);
sMap.dump();
{
constexpr std::array kKeys{
uint256{"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
};
constexpr std::array kHashes{
uint256{"B7387CFEA0465759ADC718E8C42B52D2309D179B326E239EB5075C64B6281F7F"},
uint256{"FBC195A9592A54AB44010274163CB6BA95F497EC5BA0A8831845467FB2ECE266"},
uint256{"4E7D2684B65DFD48937FFB775E20175C43AF0C94066F7D5679F51AE756795B75"},
uint256{"7A2F312EB203695FFD164E038E281839EEF06A1B99BFC263F3CECC6C74F93E07"},
uint256{"395A6691A372387A703FB0F2C6D2C405DAF307D0817F8F0E207596462B0E3A3E"},
uint256{"D044C0A696DE3169CC70AE216A1564D69DE96582865796142CE7D98A84D9DDE4"},
uint256{"76DCC77C4027309B5A91AD164083264D70B77B5E43E08AEDA5EBF94361143615"},
uint256{"DF4220E93ADC6F5569063A01B4DC79F8DB9553B6A3222ADE23DEA02BBE7230E5"},
};
SHAMap map{SHAMapType::FREE, f};
if (!testMode.backed)
map.setUnbacked();
EXPECT_EQ(map.getHash(), beast::kZero);
for (std::size_t k = 0; k < kKeys.size(); ++k)
{
EXPECT_TRUE(map.addItem(
SHAMapNodeType::TnTransactionNm,
makeShamapitem(kKeys[k], intToVuc(static_cast<std::uint8_t>(k)))));
EXPECT_EQ(map.getHash().asUInt256(), kHashes[k]);
map.invariants();
}
for (std::size_t k = kKeys.size(); k-- > 0;)
{
EXPECT_EQ(map.getHash().asUInt256(), kHashes[k]);
EXPECT_TRUE(map.delItem(kKeys[k]));
map.invariants();
}
EXPECT_EQ(map.getHash(), beast::kZero);
}
{
constexpr std::array kKeys{
uint256{"f22891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92881fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92791fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b92691fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"b91891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
uint256{"292891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8"},
};
tests::TestNodeFamily tf{j_};
SHAMap map{SHAMapType::FREE, tf};
if (!testMode.backed)
map.setUnbacked();
for (auto const& k : kKeys)
{
map.addItem(SHAMapNodeType::TnTransactionNm, makeShamapitem(k, intToVuc(0)));
map.invariants();
}
int h = 7;
for (auto const& k : map)
{
EXPECT_EQ(k.key(), kKeys[h]);
--h;
}
}
}
INSTANTIATE_TEST_SUITE_P(
BackingMode,
SHAMapTest,
::testing::Values(kBackedMode, kUnbackedMode),
shamapBackingModeName);
class SHAMapPathProof : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
};
TEST_F(SHAMapPathProof, verify_proof_path)
{
tests::TestNodeFamily tf{j_};
SHAMap map{SHAMapType::FREE, tf};
map.setUnbacked();
uint256 key;
uint256 rootHash;
std::vector<Blob> goodPath;
for (unsigned char c = 1; c < 100; ++c)
{
uint256 k(c);
map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()}));
map.invariants();
auto root = map.getHash().asUInt256();
auto path = map.getProofPath(k);
if (!path)
{
ADD_FAILURE() << "Missing proof path";
return;
}
auto& proofPath = *path;
EXPECT_TRUE(map.verifyProofPath(root, k, proofPath));
if (c == 1)
{
// extra node
proofPath.insert(proofPath.begin(), proofPath.front());
EXPECT_FALSE(map.verifyProofPath(root, k, proofPath));
// wrong key
uint256 const wrongKey(c + 1);
EXPECT_FALSE(map.getProofPath(wrongKey));
}
if (c == 99)
{
key = k;
rootHash = root;
goodPath = std::move(proofPath);
}
}
// still good
EXPECT_TRUE(map.verifyProofPath(rootHash, key, goodPath));
// empty path
std::vector<Blob> badPath;
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// too long
badPath = goodPath;
badPath.push_back(goodPath.back());
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// bad node
badPath.clear();
badPath.emplace_back(100, 100);
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// bad node type
badPath.clear();
badPath.push_back(goodPath.front());
badPath.front().back()--; // change node type
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
// all inner
badPath.clear();
badPath = goodPath;
badPath.erase(badPath.begin());
EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
}
} // namespace xrpl::tests

View File

@@ -0,0 +1,176 @@
#include <xrpl/basics/SHAMapHash.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapItem.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <shamap/common.h>
#include <chrono>
#include <cstdint>
#include <list>
#include <utility>
#include <vector>
namespace xrpl::tests {
class SHAMapSyncTest : public ::testing::Test
{
protected:
beast::Journal const j_{TestSink::instance()};
beast::xor_shift_engine eng_;
boost::intrusive_ptr<SHAMapItem>
makeRandomAS()
{
Serializer s;
for (int d = 0; d < 3; ++d)
s.add32(randInt<std::uint32_t>(eng_));
return makeShamapitem(s.getSHA512Half(), s.slice());
}
bool
confuseMap(SHAMap& map, int count)
{
// add a bunch of random states to a map, then remove them
// map should be the same
SHAMapHash const beforeHash = map.getHash();
std::list<uint256> items;
for (int i = 0; i < count; ++i)
{
auto item = makeRandomAS();
items.push_back(item->key());
if (!map.addItem(SHAMapNodeType::TnAccountState, item))
{
ADD_FAILURE() << "Unable to add item to map";
return false;
}
}
for (auto const& item : items)
{
if (!map.delItem(item))
{
ADD_FAILURE() << "Unable to remove item from map";
return false;
}
}
if (beforeHash != map.getHash())
{
ADD_FAILURE() << "Hashes do not match " << beforeHash << " " << map.getHash();
return false;
}
return true;
}
};
TEST_F(SHAMapSyncTest, sync)
{
TestNodeFamily f{j_}, f2{j_};
SHAMap source{SHAMapType::FREE, f};
SHAMap destination{SHAMapType::FREE, f2};
int const items = 10000;
for (int i = 0; i < items; ++i)
{
source.addItem(SHAMapNodeType::TnAccountState, makeRandomAS());
if (i % 100 == 0)
source.invariants();
}
source.invariants();
ASSERT_TRUE(confuseMap(source, 500));
source.invariants();
source.setImmutable();
int count = 0;
source.visitLeaves([&count]([[maybe_unused]] auto const& item) { ++count; });
EXPECT_EQ(count, items);
std::vector<SHAMapMissingNode> missingNodes;
source.walkMap(missingNodes, 2048);
EXPECT_TRUE(missingNodes.empty());
destination.setSynching();
{
std::vector<SHAMapNodeData> a;
ASSERT_TRUE(source.getNodeFat(SHAMapNodeID(), a, randBool(eng_), randInt(eng_, 2)));
ASSERT_FALSE(a.empty()) << "NodeSize";
auto node = SHAMapTreeNode::makeFromWire(makeSlice(a[0].data));
if (!node)
FAIL() << "Could not create node";
ASSERT_TRUE(destination.addRootNode(source.getHash(), std::move(node), nullptr).isGood());
}
do
{
f.clock().advance(std::chrono::seconds(1));
// get the list of nodes we know we need
auto nodesMissing = destination.getMissingNodes(2048, nullptr);
if (nodesMissing.empty())
break;
// get as many nodes as possible based on this information
std::vector<SHAMapNodeData> b;
for (auto& it : nodesMissing)
{
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (!source.getNodeFat(it.first, b, randBool(eng_), randInt(eng_, 2)))
FAIL() << "Unable to fetch node";
}
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
if (b.empty())
FAIL() << "No nodes returned";
for (auto const& i : b)
{
// Keep failures fatal here because this loop is data-dependent.
// non-deterministic number of times and the number of tests run
// should be deterministic
auto node = SHAMapTreeNode::makeFromWire(makeSlice(i.data));
if (!node)
FAIL() << "Could not create node";
if (i.isLeaf != node->isLeaf())
FAIL() << "Node is not a leaf";
if (!destination.addKnownNode(i.nodeID, std::move(node), nullptr).isUseful())
FAIL() << "Known node was not useful";
}
} while (true);
destination.clearSynching();
EXPECT_TRUE(source.deepCompare(destination));
destination.invariants();
}
} // namespace xrpl::tests

View File

@@ -4,7 +4,6 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/clock/manual_clock.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/config/Constants.h>
@@ -92,13 +91,17 @@ public:
}
void
missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) override
missingNodeAcquireBySeq(
[[maybe_unused]] std::uint32_t refNum,
[[maybe_unused]] uint256 const& nodeHash) override
{
Throw<std::runtime_error>("missing node");
}
void
missingNodeAcquireByHash(uint256 const& refHash, std::uint32_t refNum) override
missingNodeAcquireByHash(
[[maybe_unused]] uint256 const& refHash,
[[maybe_unused]] std::uint32_t refNum) override
{
Throw<std::runtime_error>("missing node");
}
@@ -110,7 +113,7 @@ public:
(*tnCache_).reset();
}
beast::ManualClock<std::chrono::steady_clock>
TestStopwatch&
clock()
{
return clock_;

View File

@@ -6,7 +6,6 @@
#include <xrpld/overlay/PeerSet.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/clock/abstract_clock.h>
#include <xrpl/json/json_value.h>
@@ -24,7 +23,7 @@
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
@@ -148,16 +147,19 @@ private:
processData(std::shared_ptr<Peer> peer, protocol::TMLedgerData const& data);
bool
takeHeader(std::string const& data);
takeHeader(std::string_view data);
void
receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode&);
receiveNode(
std::shared_ptr<Peer> const& peer,
protocol::TMLedgerData const& packet,
SHAMapAddNode& san);
bool
takeTxRootNode(Slice const& data, SHAMapAddNode&);
takeTxRootNode(std::string_view data, SHAMapAddNode& san);
bool
takeAsRootNode(Slice const& data, SHAMapAddNode&);
takeAsRootNode(std::string_view data, SHAMapAddNode& san);
std::vector<uint256>
neededTxHashes(int max, SHAMapSyncFilter const* filter) const;

View File

@@ -0,0 +1,52 @@
#pragma once
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <optional>
#include <string_view>
namespace protocol {
class TMLedgerNode;
} // namespace protocol
namespace xrpl {
/**
* @brief Deserializes a SHAMapTreeNode from wire format data.
*
* This function attempts to create a SHAMapTreeNode from the provided data string. If the data is
* malformed or deserialization fails, the function returns a nullptr instead of throwing an
* exception.
*
* @param data The serialized node data in wire format.
* @return The deserialized tree node if successful, or a nullptr if deserialization fails.
*/
[[nodiscard]] SHAMapTreeNodePtr
getTreeNode(std::string_view data);
/**
* @brief Extracts or reconstructs the SHAMapNodeID from a ledger node proto message.
*
* This function retrieves the SHAMapNodeID for a tree node, with behavior that depends on which
* field is set and the node type (inner vs. leaf).
*
* When the legacy `nodeid` field is set in the message:
* - For all nodes: Deserializes the node ID from the field.
* - For leaf nodes: Validates that the node ID is consistent with the leaf's key.
*
* When the new `id` or `depth` field is set in the message:
* - For inner nodes: Deserializes the node ID from the `id` field.
* - For leaf nodes: Reconstructs the node ID using both the depth from the `depth` field and the
* key from the leaf node's item.
* Note that root nodes may be inner nodes or leaf nodes.
*
* @param ledgerNode The validated protocol message containing the ledger node data.
* @param treeNode The deserialized tree node (inner or leaf node).
* @return An optional containing the node ID if extraction/reconstruction succeeds, or std::nullopt
* if the required fields are missing or validation fails.
*/
[[nodiscard]] std::optional<SHAMapNodeID>
getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const& treeNode);
} // namespace xrpl

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/ledger/AccountStateSF.h>
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/LedgerNodeHelpers.h>
#include <xrpld/app/ledger/TransactionStateSF.h>
#include <xrpld/app/ledger/detail/TimeoutCounter.h>
#include <xrpld/app/main/Application.h>
@@ -44,8 +45,8 @@
#include <mutex>
#include <random>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <utility>
@@ -774,7 +775,7 @@ InboundLedger::filterNodes(
*/
// data must not have hash prefix
bool
InboundLedger::takeHeader(std::string const& data)
InboundLedger::takeHeader(std::string_view data)
{
// Return value: true=normal, false=bad data
JLOG(journal_.trace()) << "got header acquiring ledger " << hash_;
@@ -819,7 +820,10 @@ InboundLedger::takeHeader(std::string const& data)
Call with a lock
*/
void
InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& san)
InboundLedger::receiveNode(
std::shared_ptr<Peer> const& peer,
protocol::TMLedgerData const& packet,
SHAMapAddNode& san)
{
if (!haveHeader_)
{
@@ -862,32 +866,47 @@ InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode&
{
auto const f = filter.get();
for (auto const& node : packet.nodes())
for (auto const& ledgerNode : packet.nodes())
{
auto const nodeID = deserializeSHAMapNodeID(node.nodeid());
auto treeNode = getTreeNode(ledgerNode.nodedata());
if (!treeNode)
{
JLOG(journal_.warn()) << "Got invalid node data";
peer->charge(Resource::kFeeInvalidData, "ledger_node.node_data invalid");
san.incInvalid();
return;
}
auto const nodeID = getSHAMapNodeID(ledgerNode, *treeNode);
if (!nodeID)
throw std::runtime_error("data does not properly deserialize");
{
JLOG(journal_.warn()) << "Got invalid node id";
peer->charge(Resource::kFeeInvalidData, "ledger_node.node_id invalid");
san.incInvalid();
return;
}
if (nodeID->isRoot())
{
san += map.addRootNode(rootHash, makeSlice(node.nodedata()), f);
san += map.addRootNode(rootHash, std::move(treeNode), f);
}
else
{
san += map.addKnownNode(*nodeID, makeSlice(node.nodedata()), f);
san += map.addKnownNode(*nodeID, std::move(treeNode), f);
}
if (!san.isGood())
{
JLOG(journal_.warn()) << "Received bad node data";
JLOG(journal_.warn()) << "Got invalid node";
peer->charge(Resource::kFeeInvalidData, "ledger_node invalid");
return;
}
}
}
catch (std::exception const& e)
{
JLOG(journal_.error()) << "Received bad node data: " << e.what();
// If we get here it is not necessarily because the node was bad, so don't charge the peer.
JLOG(journal_.error()) << "Could not process node: " << e.what();
san.incInvalid();
return;
}
@@ -915,7 +934,7 @@ InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode&
Call with a lock
*/
bool
InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san)
InboundLedger::takeAsRootNode(std::string_view data, SHAMapAddNode& san)
{
if (failed_ || haveState_)
{
@@ -931,9 +950,17 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san)
// LCOV_EXCL_STOP
}
auto treeNode = getTreeNode(data);
if (!treeNode)
{
JLOG(journal_.warn()) << "Got invalid node data";
san.incInvalid();
return false;
}
AccountStateSF filter(ledger_->stateMap().family().db(), app_.getLedgerMaster());
san +=
ledger_->stateMap().addRootNode(SHAMapHash{ledger_->header().accountHash}, data, &filter);
san += ledger_->stateMap().addRootNode(
SHAMapHash{ledger_->header().accountHash}, std::move(treeNode), &filter);
return san.isGood();
}
@@ -941,7 +968,7 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san)
Call with a lock
*/
bool
InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san)
InboundLedger::takeTxRootNode(std::string_view data, SHAMapAddNode& san)
{
if (failed_ || haveTransactions_)
{
@@ -957,8 +984,17 @@ InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san)
// LCOV_EXCL_STOP
}
auto treeNode = getTreeNode(data);
if (!treeNode)
{
JLOG(journal_.warn()) << "Got invalid node data";
san.incInvalid();
return false;
}
TransactionStateSF filter(ledger_->txMap().family().db(), app_.getLedgerMaster());
san += ledger_->txMap().addRootNode(SHAMapHash{ledger_->header().txHash}, data, &filter);
san += ledger_->txMap().addRootNode(
SHAMapHash{ledger_->header().txHash}, std::move(treeNode), &filter);
return san.isGood();
}
@@ -1055,15 +1091,25 @@ InboundLedger::processData(std::shared_ptr<Peer> peer, protocol::TMLedgerData co
}
if (!haveState_ && (packet.nodes().size() > 1) &&
!takeAsRootNode(makeSlice(packet.nodes(1).nodedata()), san))
!takeAsRootNode(packet.nodes(1).nodedata(), san))
{
JLOG(journal_.warn()) << "Included AS root invalid";
if (san.isInvalid())
{
peer->charge(Resource::kFeeInvalidData, "ledger_data invalid AS root");
return -1;
}
}
if (!haveTransactions_ && (packet.nodes().size() > 2) &&
!takeTxRootNode(makeSlice(packet.nodes(2).nodedata()), san))
!takeTxRootNode(packet.nodes(2).nodedata(), san))
{
JLOG(journal_.warn()) << "Included TX root invalid";
if (san.isInvalid())
{
peer->charge(Resource::kFeeInvalidData, "ledger_data invalid TX root");
return -1;
}
}
}
catch (std::exception const& ex)
@@ -1092,24 +1138,18 @@ InboundLedger::processData(std::shared_ptr<Peer> peer, protocol::TMLedgerData co
ScopedLockType const sl(mtx_);
// Verify node IDs and data are complete
for (auto const& node : packet.nodes())
{
if (!node.has_nodeid() || !node.has_nodedata())
{
JLOG(journal_.warn()) << "Got bad node";
peer->charge(Resource::kFeeMalformedRequest, "ledger_data bad node");
return -1;
}
}
SHAMapAddNode san;
receiveNode(packet, san);
receiveNode(peer, packet, san);
JLOG(journal_.debug()) << "Ledger "
<< ((packet.type() == protocol::liTX_NODE) ? "TX" : "AS")
<< " node stats: " << san.get();
// Note: Peer charges for invalid/malformed data are issued from within receiveNode at the
// exact failure site, so the peer is only charged for problems they are responsible for.
if (san.isInvalid())
return -1;
if (san.isUseful())
progress_ = true;

View File

@@ -2,13 +2,13 @@
#include <xrpld/app/ledger/InboundLedger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/LedgerNodeHelpers.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/overlay/PeerSet.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/DecayingSample.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/scope.h>
@@ -248,23 +248,17 @@ public:
Serializer s;
try
{
for (int i = 0; i < packetPtr->nodes().size(); ++i)
for (auto const& ledgerNode : packetPtr->nodes())
{
auto const& node = packetPtr->nodes(i);
if (!node.has_nodeid() || !node.has_nodedata())
return;
auto newNode = SHAMapTreeNode::makeFromWire(makeSlice(node.nodedata()));
if (!newNode)
auto const treeNode = getTreeNode(ledgerNode.nodedata());
if (!treeNode)
return;
s.erase();
newNode->serializeWithPrefix(s);
treeNode->serializeWithPrefix(s);
app_.getLedgerMaster().addFetchPack(
newNode->getHash().asUInt256(), std::make_shared<Blob>(s.begin(), s.end()));
treeNode->getHash().asUInt256(), std::make_shared<Blob>(s.begin(), s.end()));
}
}
catch (std::exception const&) // NOLINT(bugprone-empty-catch)

View File

@@ -1,11 +1,11 @@
#include <xrpld/app/ledger/InboundTransactions.h>
#include <xrpld/app/ledger/LedgerNodeHelpers.h>
#include <xrpld/app/ledger/detail/TransactionAcquire.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/overlay/PeerSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/beast/insight/Collector.h>
#include <xrpl/protocol/RippleLedgerHash.h>
@@ -14,6 +14,7 @@
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <xrpl.pb.h>
@@ -136,34 +137,43 @@ public:
if (ta == nullptr)
{
peer->charge(Resource::kFeeUselessData, "ledger_data");
peer->charge(Resource::kFeeUselessData, "ledger_data useless");
return;
}
std::vector<std::pair<SHAMapNodeID, Slice>> data;
std::vector<std::pair<SHAMapNodeID, SHAMapTreeNodePtr>> data;
data.reserve(packet.nodes().size());
for (auto const& node : packet.nodes())
for (auto const& ledgerNode : packet.nodes())
{
if (!node.has_nodeid() || !node.has_nodedata())
auto treeNode = getTreeNode(ledgerNode.nodedata());
if (!treeNode)
{
peer->charge(Resource::kFeeMalformedRequest, "ledger_data");
JLOG(j_.warn()) << "Got invalid node data";
peer->charge(Resource::kFeeInvalidData, "ledger_node.node_data invalid");
return;
}
auto const id = deserializeSHAMapNodeID(node.nodeid());
if (!id)
auto const nodeID = getSHAMapNodeID(ledgerNode, *treeNode);
if (!nodeID)
{
peer->charge(Resource::kFeeInvalidData, "ledger_data");
JLOG(j_.warn()) << "Got invalid node id";
peer->charge(Resource::kFeeInvalidData, "ledger_node.node_id invalid");
return;
}
data.emplace_back(*id, makeSlice(node.nodedata()));
data.emplace_back(*nodeID, std::move(treeNode));
}
if (!ta->takeNodes(data, peer).isUseful())
peer->charge(Resource::kFeeUselessData, "ledger_data not useful");
auto const san = ta->takeNodes(std::move(data), peer);
if (san.isInvalid())
{
peer->charge(Resource::kFeeInvalidData, "ledger_data invalid");
}
else if (!san.isUseful())
{
peer->charge(Resource::kFeeUselessData, "ledger_data useless");
}
}
void

View File

@@ -0,0 +1,81 @@
#include <xrpld/app/ledger/LedgerNodeHelpers.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapLeafNode.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <xrpl.pb.h>
#include <exception>
#include <optional>
#include <string_view>
namespace xrpl {
SHAMapTreeNodePtr
getTreeNode(std::string_view data)
{
auto const slice = makeSlice(data);
try
{
return SHAMapTreeNode::makeFromWire(slice);
}
catch (std::exception const&)
{
return {};
}
}
std::optional<SHAMapNodeID>
getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const& treeNode)
{
if (ledgerNode.has_id() || ledgerNode.has_depth())
{
// Reject ambiguous messages that mix the legacy and new reference fields.
if (ledgerNode.has_nodeid())
return std::nullopt;
if (treeNode.isInner())
{
if (!ledgerNode.has_id())
return std::nullopt;
return deserializeSHAMapNodeID(ledgerNode.id());
}
if (treeNode.isLeaf())
{
if (!ledgerNode.has_depth() || ledgerNode.depth() > SHAMap::kLeafDepth)
return std::nullopt;
auto const key = safeDowncast<SHAMapLeafNode const*>(&treeNode)->peekItem()->key();
return SHAMapNodeID::createID(ledgerNode.depth(), key);
}
UNREACHABLE("xrpl::getSHAMapNodeID : tree node is neither inner nor leaf");
return std::nullopt;
}
if (!ledgerNode.has_nodeid())
return std::nullopt;
auto nodeID = deserializeSHAMapNodeID(ledgerNode.nodeid());
if (!nodeID.has_value())
return std::nullopt;
if (treeNode.isLeaf())
{
auto const key = safeDowncast<SHAMapLeafNode const*>(&treeNode)->peekItem()->key();
auto const expectedID = SHAMapNodeID::createID(static_cast<int>(nodeID->getDepth()), key);
if (nodeID->getNodeID() != expectedID.getNodeID())
return std::nullopt;
}
return nodeID;
}
} // namespace xrpl

View File

@@ -7,13 +7,13 @@
#include <xrpld/overlay/PeerSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/core/Job.h>
#include <xrpl/server/NetworkOPs.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapAddNode.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <xrpl.pb.h>
@@ -171,7 +171,7 @@ TransactionAcquire::trigger(std::shared_ptr<Peer> const& peer)
SHAMapAddNode
TransactionAcquire::takeNodes(
std::vector<std::pair<SHAMapNodeID, Slice>> const& data,
std::vector<std::pair<SHAMapNodeID, SHAMapTreeNodePtr>> data,
std::shared_ptr<Peer> const& peer)
{
ScopedLockType const sl(mtx_);
@@ -195,7 +195,7 @@ TransactionAcquire::takeNodes(
ConsensusTransSetSF sf(app_, app_.getTempNodeCache());
for (auto const& d : data)
for (auto& d : data)
{
if (d.first.isRoot())
{
@@ -203,7 +203,8 @@ TransactionAcquire::takeNodes(
{
JLOG(journal_.debug()) << "Got root TXS node, already have it";
}
else if (!map_->addRootNode(SHAMapHash{hash_}, d.second, nullptr).isGood())
else if (!map_->addRootNode(SHAMapHash{hash_}, std::move(d.second), nullptr)
.isGood())
{
JLOG(journal_.warn()) << "TX acquire got bad root node";
}
@@ -212,7 +213,7 @@ TransactionAcquire::takeNodes(
haveRoot_ = true;
}
}
else if (!map_->addKnownNode(d.first, d.second, &sf).isGood())
else if (!map_->addKnownNode(d.first, std::move(d.second), &sf).isGood())
{
JLOG(journal_.warn()) << "TX acquire got bad non-root node";
return SHAMapAddNode::invalid();

View File

@@ -6,10 +6,10 @@
#include <xrpld/overlay/PeerSet.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapAddNode.h>
#include <xrpl/shamap/SHAMapTreeNode.h>
#include <cstddef>
#include <memory>
@@ -32,8 +32,8 @@ public:
SHAMapAddNode
takeNodes(
std::vector<std::pair<SHAMapNodeID, Slice>> const& data,
std::shared_ptr<Peer> const&);
std::vector<std::pair<SHAMapNodeID, SHAMapTreeNodePtr>> data,
std::shared_ptr<Peer> const& peer);
void
init(int startPeers);

View File

@@ -23,6 +23,7 @@ enum class ProtocolFeature {
ValidatorListPropagation,
ValidatorList2Propagation,
LedgerReplay,
LedgerNodeDepth,
};
/** Represents a peer connection in the overlay. */

View File

@@ -5,6 +5,7 @@
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/InboundTransactions.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/LedgerNodeHelpers.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/ValidatorList.h>
@@ -61,6 +62,7 @@
#include <xrpl/resource/Gossip.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/server/NetworkOPs.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/tx/apply.h>
@@ -539,6 +541,8 @@ PeerImp::supportsFeature(ProtocolFeature f) const
return protocol_ >= makeProtocol(2, 1);
case ProtocolFeature::ValidatorList2Propagation:
return protocol_ >= makeProtocol(2, 2);
case ProtocolFeature::LedgerNodeDepth:
return protocol_ >= makeProtocol(2, 3);
case ProtocolFeature::LedgerReplay:
return ledgerReplayEnabled_;
}
@@ -1473,23 +1477,12 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
}
}
// Verify ledger node IDs
if (itype != protocol::liBASE)
// Verify ledger node counts. Full parsing of the node IDs is deferred to the job, so the I/O
// thread is not burdened with SHAMapNodeID deserialization for every TMGetLedger message.
if (itype != protocol::liBASE && m->nodeids_size() <= 0)
{
if (m->nodeids_size() <= 0)
{
badData("Invalid ledger node IDs");
return;
}
for (auto const& nodeId : m->nodeids())
{
if (deserializeSHAMapNodeID(nodeId) == std::nullopt)
{
badData("Invalid SHAMap node ID");
return;
}
}
badData("Invalid ledger node IDs");
return;
}
// Verify query type
@@ -1509,11 +1502,40 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
}
}
// Queue a job to process the request
// Queue a job to process the request.
std::weak_ptr<PeerImp> const weak = shared_from_this();
app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m]() {
if (auto peer = weak.lock())
peer->processLedgerRequest(m);
app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m, itype]() {
auto peer = weak.lock();
if (!peer)
return;
std::vector<SHAMapNodeID> nodeIDs;
if (itype != protocol::liBASE)
{
nodeIDs.reserve(std::min(m->nodeids_size(), Tuning::kSoftMaxReplyNodes));
for (auto const& nodeId : m->nodeids())
{
if (nodeIDs.size() >= Tuning::kSoftMaxReplyNodes)
{
// Charge the peer for requesting too many node IDs, but continue processing the
// received node IDs up to the limit. If the request is legitimate then at least
// they will get a response and won't have to resend these nodes in their next
// request.
peer->charge(
Resource::kFeeModerateBurdenPeer, "TMGetLedger: too many node IDs");
break;
}
auto parsed = deserializeSHAMapNodeID(nodeId);
if (!parsed)
{
peer->charge(Resource::kFeeInvalidData, "TMGetLedger: Invalid node ID");
return;
}
nodeIDs.push_back(std::move(*parsed));
}
}
peer->processLedgerRequest(m, std::move(nodeIDs));
});
}
@@ -1678,12 +1700,44 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMLedgerData> const& m)
return;
}
// If there is a request cookie, attempt to relay the message
// If there is a request cookie, attempt to relay the message.
if (m->has_requestcookie())
{
if (auto peer = overlay_.findPeerByShortID(m->requestcookie()))
{
m->clear_requestcookie();
// If the original requester doesn't support the new depth-based format, rewrite any
// nodes that use it back to the legacy nodeid format before relaying. Once all nodes
// have upgraded, the old protocol version and this code can be removed.
if (!peer->supportsFeature(ProtocolFeature::LedgerNodeDepth))
{
for (int i = 0; i < m->nodes_size(); ++i)
{
auto* ledgerNode = m->mutable_nodes(i);
if (ledgerNode->reference_case() != ledgerNode->REFERENCE_NOT_SET)
{
auto treeNode = getTreeNode(ledgerNode->nodedata());
if (!treeNode)
{
JLOG(pJournal_.warn()) << "Unable to get tree node";
return;
}
auto const nodeID = getSHAMapNodeID(*ledgerNode, *treeNode);
if (!nodeID)
{
JLOG(pJournal_.warn()) << "Unable to get node ID";
return;
}
ledgerNode->set_nodeid(nodeID->getRawString());
ledgerNode->clear_id();
ledgerNode->clear_depth();
}
}
}
peer->send(std::make_shared<Message>(*m, protocol::mtLEDGER_DATA));
}
else
@@ -3283,7 +3337,9 @@ PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const
}
void
PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
PeerImp::processLedgerRequest(
std::shared_ptr<protocol::TMGetLedger> const& m,
std::vector<SHAMapNodeID> nodeIDs)
{
// Do not resource charge a peer responding to a relay
if (!m->has_requestcookie())
@@ -3368,26 +3424,25 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
}
// Add requested node data to reply
if (m->nodeids_size() > 0)
if (!nodeIDs.empty())
{
std::uint32_t const defaultDepth = isHighLatency() ? 2 : 1;
auto const queryDepth{m->has_querydepth() ? m->querydepth() : defaultDepth};
std::vector<std::pair<SHAMapNodeID, Blob>> data;
std::vector<SHAMapNodeData> data;
data.reserve(Tuning::kSoftMaxReplyNodes);
auto const useLedgerNodeDepth = supportsFeature(ProtocolFeature::LedgerNodeDepth);
for (int i = 0;
i < m->nodeids_size() && ledgerData.nodes_size() < Tuning::kSoftMaxReplyNodes;
++i)
for (auto const& nodeID : nodeIDs)
{
auto const shaMapNodeId{deserializeSHAMapNodeID(m->nodeids(i))};
if (ledgerData.nodes_size() >= Tuning::kSoftMaxReplyNodes)
break;
data.clear();
data.reserve(Tuning::kSoftMaxReplyNodes);
try
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) nodeids checked in onGetLedger
if (map->getNodeFat(*shaMapNodeId, data, fatLeaves, queryDepth))
if (map->getNodeFat(nodeID, data, fatLeaves, queryDepth))
{
JLOG(pJournal_.trace())
<< "processLedgerRequest: getNodeFat got " << data.size() << " nodes";
@@ -3396,9 +3451,25 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
{
if (ledgerData.nodes_size() >= Tuning::kHardMaxReplyNodes)
break;
protocol::TMLedgerNode* node{ledgerData.add_nodes()};
node->set_nodeid(d.first.getRawString());
node->set_nodedata(d.second.data(), d.second.size());
node->set_nodedata(d.data.data(), d.data.size());
// When the LedgerNodeDepth protocol feature is not supported by the peer,
// we always set the `nodeid` field. However, when it is supported then we
// set the `id` field for inner nodes and the `depth` field for leaf nodes.
if (!useLedgerNodeDepth)
{
node->set_nodeid(d.nodeID.getRawString());
}
else if (d.isLeaf)
{
node->set_depth(d.nodeID.getDepth());
}
else
{
node->set_id(d.nodeID.getRawString());
}
}
}
else
@@ -3437,7 +3508,7 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
info += ", no hash specified";
JLOG(pJournal_.warn())
<< "processLedgerRequest: getNodeFat with nodeId " << *shaMapNodeId
<< "processLedgerRequest: getNodeFat with nodeId " << nodeID
<< " and ledger info type " << info << " throws exception: " << e.what();
}
}

View File

@@ -32,6 +32,7 @@
#include <xrpl/resource/Consumer.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/server/Handoff.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <boost/circular_buffer.hpp>
#include <boost/endian/conversion.hpp>
@@ -659,7 +660,9 @@ private:
getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const;
void
processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m);
processLedgerRequest(
std::shared_ptr<protocol::TMGetLedger> const& m,
std::vector<SHAMapNodeID> nodeIDs);
protected:
// Kept `protected` so test subclasses (see

View File

@@ -28,6 +28,7 @@ namespace xrpl {
constexpr ProtocolVersion const kSupportedProtocolList[]{
{2, 1},
{2, 2},
{2, 3},
};
// This ugly construct ensures that supportedProtocolList is sorted in strictly