Compare commits

..

4 Commits

Author SHA1 Message Date
Jingchen
7432427045 Merge branch 'develop' into a1q123456/add-loan-invariants 2026-07-06 17:21:22 +01:00
JCW
4d92e6a19d Address a comment 2026-07-06 17:20:02 +01:00
JCW
6647b60167 Address a comment 2026-07-06 17:19:36 +01:00
JCW
e538aeb59a Add vault invariants 2026-07-06 15:35:51 +01:00
47 changed files with 6010 additions and 4956 deletions

View File

@@ -45,10 +45,20 @@ 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

@@ -356,3 +356,4 @@ words:
- xxhash
- xxhasher
- CGNAT
- unimpairs

View File

@@ -72,6 +72,7 @@ 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
@@ -161,6 +162,9 @@ 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
@@ -184,6 +188,12 @@ 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
@@ -195,7 +205,6 @@ 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

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

View File

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

View File

@@ -555,11 +555,7 @@ 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>);
@@ -804,7 +800,6 @@ 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

@@ -36,6 +36,20 @@ namespace xrpl {
* - vault withdrawal and clawback reduce assets and share issuance, and
* subtracts from: total assets, assets available, shares outstanding
* - vault set must not alter the vault assets or shares balance
* - loan set moves the requested principal out of the vault: it must create
* exactly one loan, decreases assets available (and the vault balance) by the
* principal, grows assets outstanding by exactly the interest due booked on
* the created loan, and does not change shares
* - loan manage never removes assets from the vault: assets available may only
* grow (and the vault balance grows with it, by the returned first-loss
* capital on a default), assets outstanding may only shrink (realized loss),
* loss unrealized stays non-negative, and shares do not change
* - loan pay adds the paid principal and interest to the vault: assets
* available (and the vault balance) increase by the same amount, which is at
* most the amount paid, loss unrealized stays non-negative, and shares do not
* change; and assets outstanding move in lock-step: their change equals the
* cash received plus the change in the paid loan's claim on the vault, which
* verifies the payment was split correctly between principal and interest
* - no vault transaction can change loss unrealized (it's updated by loan
* transactions)
*
@@ -55,6 +69,8 @@ class ValidVault
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
std::uint8_t withdrawalPolicy = 0;
std::uint8_t scale = 0;
Vault static make(SLE const&);
};
@@ -68,6 +84,26 @@ class ValidVault
Shares static make(SLE const&);
};
struct Loan final
{
uint256 key = beast::kZero;
Number principalOutstanding = 0;
Number totalValueOutstanding = 0;
Number managementFeeOutstanding = 0;
// Interest booked to the vault at loan creation: the portion of the
// total value owed that is neither principal nor broker management fee.
[[nodiscard]] Number
interestDue() const;
// The vault's claim on the loan: the total value owed less the broker's
// management fee (which belongs to the broker, not the vault).
[[nodiscard]] Number
claim() const;
Loan static make(SLE const&);
};
public:
struct DeltaInfo final
{
@@ -82,8 +118,10 @@ public:
private:
std::vector<Vault> afterVault_;
std::vector<Shares> afterMPTs_;
std::vector<Loan> afterLoan_;
std::vector<Vault> beforeVault_;
std::vector<Shares> beforeMPTs_;
std::vector<Loan> beforeLoan_;
std::unordered_map<uint256, DeltaInfo> deltas_;
/**

View File

@@ -17,6 +17,7 @@
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
@@ -44,6 +45,8 @@ ValidVault::Vault::make(SLE const& from)
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
self.withdrawalPolicy = from.at(sfWithdrawalPolicy);
self.scale = from.at(sfScale);
return self;
}
@@ -61,6 +64,31 @@ ValidVault::Shares::make(SLE const& from)
return self;
}
Number
ValidVault::Loan::interestDue() const
{
return totalValueOutstanding - principalOutstanding - managementFeeOutstanding;
}
Number
ValidVault::Loan::claim() const
{
return totalValueOutstanding - managementFeeOutstanding;
}
ValidVault::Loan
ValidVault::Loan::make(SLE const& from)
{
XRPL_ASSERT(from.getType() == ltLOAN, "ValidVault::Loan::make : from Loan object");
ValidVault::Loan self;
self.key = from.key();
self.principalOutstanding = from.at(sfPrincipalOutstanding);
self.totalValueOutstanding = from.at(sfTotalValueOutstanding);
self.managementFeeOutstanding = from.at(sfManagementFeeOutstanding);
return self;
}
void
ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
@@ -119,6 +147,12 @@ ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref afte
sign = -1;
break;
}
case ltLOAN:
// A loan carries no vault balance of its own; capture its prior
// state so the loan pay invariant can verify the change in the
// vault's claim on the loan.
beforeLoan_.push_back(Loan::make(*before));
break;
default:;
}
}
@@ -164,6 +198,11 @@ ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref afte
sign = -1;
break;
}
case ltLOAN:
// A loan carries no vault balance of its own; capture it so the
// loan set invariant can verify the interest booked to the vault.
afterLoan_.push_back(Loan::make(*after));
break;
default:;
}
}
@@ -423,7 +462,10 @@ ValidVault::finalize(
{
auto const& beforeVault = beforeVault_[0];
if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
afterVault.shareMPTID != beforeVault.shareMPTID)
afterVault.shareMPTID != beforeVault.shareMPTID ||
afterVault.owner != beforeVault.owner ||
afterVault.withdrawalPolicy != beforeVault.withdrawalPolicy ||
afterVault.scale != beforeVault.scale)
{
JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data";
result = false;
@@ -1042,11 +1084,314 @@ ValidVault::finalize(
return result;
}
case ttLOAN_SET:
case ttLOAN_MANAGE:
case ttLOAN_SET: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : loan set updated a vault");
auto const& beforeVault = beforeVault_[0];
// Funding a loan moves the requested principal out of the vault
// pseudo-account to the borrower (and, if any, the origination
// fee to the broker owner).
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: loan set must change vault balance";
return false; // That's all we can do
}
// Get the posterior scale to round calculations to
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
// The vault only releases the requested principal, which must
// match the reduction of both the vault (pseudo-account) balance
// and the assets available.
auto const principalDelta =
roundToAsset(vaultAsset, -tx[sfPrincipalRequested], minScale);
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets != principalDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: loan set must decrease vault balance "
"by the principal requested";
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != principalDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: loan set must decrease assets "
"available by the principal requested";
result = false;
}
// A loan set must create exactly one loan object; the interest
// it books is the only permitted change to assets outstanding.
if (afterLoan_.size() != 1 || !beforeLoan_.empty())
{
JLOG(j.fatal()) << //
"Invariant failed: loan set must create exactly one loan";
return false; // That's all we can do
}
auto const& loan = afterLoan_[0];
// Interest accrues to the vault: assets outstanding must grow by
// exactly the interest due booked on the newly created loan.
auto const assetsTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
auto const interestDue = roundToAsset(vaultAsset, loan.interestDue(), minScale);
if (assetsTotalDelta != interestDue)
{
JLOG(j.fatal()) << //
"Invariant failed: loan set must increase assets "
"outstanding by the interest due";
result = false;
}
if (afterVault.assetsMaximum > kZero &&
afterVault.assetsTotal > afterVault.assetsMaximum)
{
JLOG(j.fatal()) << //
"Invariant failed: loan set assets outstanding must not "
"exceed assets maximum";
result = false;
}
// A loan set neither mints nor burns vault shares.
if (beforeShares && updatedShares &&
beforeShares->sharesTotal != updatedShares->sharesTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: loan set must not change shares outstanding";
result = false;
}
return result;
}
case ttLOAN_MANAGE: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(),
"xrpl::ValidVault::finalize : loan manage updated a vault");
auto const& beforeVault = beforeVault_[0];
// Loan management (impair / unimpair / default) never removes
// assets from the vault. Only a default returns first-loss
// capital from the broker to the vault pseudo-account; impair
// and unimpair merely adjust the paper (unrealized) loss and
// touch no balances.
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const vaultDelta = maybeVaultDeltaAssets.value_or(
DeltaInfo{.delta = kZero, .scale = scale(afterVault.assetsTotal, vaultAsset)});
// Get the posterior scale to round calculations to
auto const minScale = computeVaultMinScale(vaultDelta, view.rules());
auto const vaultDeltaAssets = roundToAsset(vaultAsset, vaultDelta.delta, minScale);
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
// --- Checks common to every loan manage sub-operation ---
// Assets available always tracks the real vault balance: any
// change to one is matched by the other.
if (assetAvailableDelta != vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: loan manage vault balance and assets "
"available must add up";
result = false;
}
// Loan management adjusts the unrealized (paper) loss but must
// never drive it negative.
if (afterVault.lossUnrealized < kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan manage must not make loss "
"unrealized negative";
result = false;
}
// A loan manage neither mints nor burns vault shares.
if (beforeShares && updatedShares &&
beforeShares->sharesTotal != updatedShares->sharesTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: loan manage must not change shares "
"outstanding";
result = false;
}
// --- Checks specific to each loan manage sub-operation ---
if (tx.isFlag(tfLoanImpair) || tx.isFlag(tfLoanUnimpair))
{
// Impair / unimpair only move the paper (unrealized) loss;
// they touch neither balances nor assets.
if (assetAvailableDelta != kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan impair/unimpair must not "
"change assets available";
result = false;
}
if (assetTotalDelta != kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan impair/unimpair must not "
"change assets outstanding";
result = false;
}
}
else if (tx.isFlag(tfLoanDefault))
{
// A default returns first-loss capital to the vault, so
// assets available (and the vault balance) may only grow.
if (assetAvailableDelta < kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan default must not decrease "
"assets available";
result = false;
}
// A default realizes (writes off) the uncovered portion of
// the loan, so assets outstanding may only shrink.
if (assetTotalDelta > kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan default must not increase "
"assets outstanding";
result = false;
}
}
return result;
}
case ttLOAN_PAY: {
// TBD
return true;
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : loan pay updated a vault");
auto const& beforeVault = beforeVault_[0];
// A loan payment moves the paid principal and interest into the
// vault pseudo-account (fees go to the broker), so the vault
// balance and the assets available both increase by that amount.
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay must change vault balance";
return false; // That's all we can do
}
// Get the posterior scale to round calculations to
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
// Assets available always tracks the real vault balance: any
// change to one is matched by the other.
if (assetAvailableDelta != vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay vault balance and assets "
"available must add up";
result = false;
}
// A payment always adds funds to the vault, so assets available
// (and the vault balance) must increase.
if (assetAvailableDelta <= kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay must increase assets "
"available";
result = false;
}
// The vault only receives the principal and interest portion of
// the borrower's payment (the fee goes to the broker), so it can
// never grow by more than the amount paid.
auto const amountPaid = roundToAsset(vaultAsset, tx[sfAmount], minScale);
if (assetAvailableDelta > amountPaid)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay must not increase assets "
"available by more than the amount paid";
result = false;
}
// A payment unimpairs the loan, so it may reduce the unrealized
// (paper) loss, but must never drive it negative.
if (afterVault.lossUnrealized < kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay must not make loss "
"unrealized negative";
result = false;
}
// A loan pay neither mints nor burns vault shares.
if (beforeShares && updatedShares &&
beforeShares->sharesTotal != updatedShares->sharesTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay must not change shares "
"outstanding";
result = false;
}
// The vault's total assets equal its available cash plus the
// claim it holds on outstanding loans (each loan's total value
// owed, less the broker's management fee, which belongs to the
// broker). A payment only moves value between those two pools,
// so the change in assets outstanding must equal the cash
// received plus the change in the paid loan's claim on the
// vault. This is an independent check that the borrower's
// payment was split correctly between principal and interest.
if (afterLoan_.size() != 1 || beforeLoan_.size() != 1 ||
afterLoan_[0].key != beforeLoan_[0].key)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay must modify exactly one "
"loan";
result = false;
}
else
{
auto const claimDelta = roundToAsset(
vaultAsset, afterLoan_[0].claim() - beforeLoan_[0].claim(), minScale);
auto const assetsTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
if (assetsTotalDelta != assetAvailableDelta + claimDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: loan pay assets outstanding must "
"match the cash received and the change in the loan "
"claim";
result = false;
}
}
return result;
}
default:

View File

@@ -2667,6 +2667,15 @@ class Invariants_test : public beast::unit_test::Suite
AccountID account;
int amount;
};
// Parameters for a synthetic loan object created alongside a vault
// adjustment. The interest due booked to the vault is
// totalValueOutstanding - principalOutstanding - managementFeeOutstanding.
struct LoanParams
{
int principalOutstanding = 0;
int totalValueOutstanding = 0;
int managementFeeOutstanding = 0;
};
struct Adjustments
{
// NOLINTBEGIN(readability-redundant-member-init)
@@ -2678,6 +2687,10 @@ class Invariants_test : public beast::unit_test::Suite
std::optional<int> vaultAssets = std::nullopt;
std::optional<AccountAmount> accountAssets = std::nullopt;
std::optional<AccountAmount> accountShares = std::nullopt;
std::optional<LoanParams> createLoan = std::nullopt;
// Number of loan objects to create (only used when createLoan is
// set); a valid loan set creates exactly one.
int loanCount = 1;
// NOLINTEND(readability-redundant-member-init)
};
constexpr auto kAdjust = [&](ApplyView& ac, xrpl::Keylet keylet, Adjustments args) {
@@ -2776,6 +2789,26 @@ class Invariants_test : public beast::unit_test::Suite
(*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
ac.update(sleMPToken);
}
if (args.createLoan)
{
auto const& lp = *args.createLoan;
bool const anyOutstanding = lp.principalOutstanding != 0 ||
lp.totalValueOutstanding != 0 || lp.managementFeeOutstanding != 0;
for (std::uint32_t seq = 1; seq <= static_cast<std::uint32_t>(args.loanCount);
++seq)
{
auto sleLoan = std::make_shared<SLE>(keylet::loan(keylet.key, seq));
sleLoan->at(sfPrincipalOutstanding) = Number(lp.principalOutstanding);
sleLoan->at(sfTotalValueOutstanding) = Number(lp.totalValueOutstanding);
sleLoan->at(sfManagementFeeOutstanding) = Number(lp.managementFeeOutstanding);
// ValidLoan requires a positive periodic payment, and that a
// loan with payments remaining is not fully paid off.
sleLoan->at(sfPeriodicPayment) = Number(1);
sleLoan->setFieldU32(sfPaymentRemaining, anyOutstanding ? 1 : 0);
ac.insert(sleLoan);
}
}
return true;
};
@@ -3361,6 +3394,411 @@ class Invariants_test : public beast::unit_test::Suite
precloseXrp,
TxAccount::A2);
testcase << "Vault loan operations";
// ttLOAN_SET: the vault (pseudo-account) balance must change
doInvariantCheck(
{"loan set must change vault balance"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, Adjustments{});
},
XRPAmount{},
STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_SET: the balance decreases, but not by the principal requested
doInvariantCheck(
{"loan set must decrease vault balance by the principal requested",
"loan set must decrease assets available by the principal requested"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsAvailable = -100,
.vaultAssets = -100,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 100},
.createLoan = LoanParams{}});
},
XRPAmount{},
STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_SET: principal matches, but no loan object is created
doInvariantCheck(
{"loan set must create exactly one loan"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsAvailable = -200,
.vaultAssets = -200,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 200}});
},
XRPAmount{},
STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_SET: principal matches, but more than one loan is created
doInvariantCheck(
{"loan set must create exactly one loan"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsAvailable = -200,
.vaultAssets = -200,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 200},
.createLoan = LoanParams{},
.loanCount = 2});
},
XRPAmount{},
STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_SET: principal matches, but assets outstanding change does not
// equal the interest due booked on the created loan (0 here)
doInvariantCheck(
{"loan set must increase assets outstanding by the interest due"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsTotal = -50,
.assetsAvailable = -200,
.vaultAssets = -200,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 200},
.createLoan = LoanParams{}});
},
XRPAmount{},
STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_SET: principal matches, but shares outstanding changes
doInvariantCheck(
{"loan set must not change shares outstanding"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsAvailable = -200,
.sharesTotal = 10,
.vaultAssets = -200,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 200},
.accountShares = AccountAmount{.account = a2.id(), .amount = 10},
.createLoan = LoanParams{}});
},
XRPAmount{},
STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_MANAGE: vault balance and assets available do not add up
doInvariantCheck(
{"loan manage vault balance and assets available must add up"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, Adjustments{.assetsAvailable = -100});
},
XRPAmount{},
STTx{ttLOAN_MANAGE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_MANAGE: loss unrealized driven negative
doInvariantCheck(
{"loan manage must not make loss unrealized negative"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, Adjustments{.lossUnrealized = -1});
},
XRPAmount{},
STTx{ttLOAN_MANAGE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_MANAGE: shares outstanding changes
doInvariantCheck(
{"loan manage must not change shares outstanding"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.sharesTotal = 10,
.accountShares = AccountAmount{.account = a2.id(), .amount = 10}});
},
XRPAmount{},
STTx{ttLOAN_MANAGE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_MANAGE (impair): assets available must not change
doInvariantCheck(
{"loan impair/unimpair must not change assets available"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsAvailable = -100,
.vaultAssets = -100,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 100}});
},
XRPAmount{},
STTx{ttLOAN_MANAGE, [](STObject& tx) { tx.setFieldU32(sfFlags, tfLoanImpair); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_MANAGE (default): assets available must not decrease
doInvariantCheck(
{"loan default must not decrease assets available"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsTotal = -100,
.assetsAvailable = -100,
.vaultAssets = -100,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 100}});
},
XRPAmount{},
STTx{ttLOAN_MANAGE, [](STObject& tx) { tx.setFieldU32(sfFlags, tfLoanDefault); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_MANAGE (default): assets outstanding must not increase
doInvariantCheck(
{"loan default must not increase assets outstanding"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsTotal = 100,
.assetsAvailable = 100,
.vaultAssets = 100,
.accountAssets = AccountAmount{.account = a2.id(), .amount = -100}});
},
XRPAmount{},
STTx{ttLOAN_MANAGE, [](STObject& tx) { tx.setFieldU32(sfFlags, tfLoanDefault); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_PAY: the vault (pseudo-account) balance must change
doInvariantCheck(
{"loan pay must change vault balance"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, Adjustments{});
},
XRPAmount{},
STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_PAY: assets available must increase
doInvariantCheck(
{"loan pay must increase assets available"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsAvailable = -100,
.vaultAssets = -100,
.accountAssets = AccountAmount{.account = a2.id(), .amount = 100}});
},
XRPAmount{},
STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_PAY: assets available increases by more than the amount paid
doInvariantCheck(
{"loan pay must not increase assets available by more than the amount paid"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsTotal = 300,
.assetsAvailable = 300,
.vaultAssets = 300,
.accountAssets = AccountAmount{.account = a2.id(), .amount = -300}});
},
XRPAmount{},
STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(100)); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_PAY: shares outstanding changes
doInvariantCheck(
{"loan pay must not change shares outstanding"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(
ac.view(),
keylet,
Adjustments{
.assetsTotal = 100,
.assetsAvailable = 100,
.sharesTotal = 10,
.vaultAssets = 100,
.accountAssets = AccountAmount{.account = a2.id(), .amount = -100},
.accountShares = AccountAmount{.account = a2.id(), .amount = 10}});
},
XRPAmount{},
STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttLOAN_PAY: assets outstanding must match the cash received and the
// change in the paid loan's claim on the vault. A payment only moves
// value between the vault's available cash and its claim on the loan,
// so bumping assets outstanding by more than that must be caught. This
// needs a loan that already exists in the base ledger (so modifying it
// is seen as a before/after change), which the shared harness cannot
// set up, hence the bespoke view construction below.
{
Env env{*this, defaultAmendments()};
Account const a1{"A1"};
Account const a2{"A2"};
env.fund(XRP(1000), a1, a2);
BEAST_EXPECT(precloseXrp(a1, a2, env));
env.close();
OpenView ov{*env.current()};
auto const vaultKeylet = keylet::vault(a1.id(), ov.seq());
// Insert a pre-existing loan into the base view; modifying it in the
// apply view is then seen as a loan modification (before/after).
auto const loanKeylet = keylet::loan(vaultKeylet.key, 1);
{
auto sleLoan = std::make_shared<SLE>(loanKeylet);
sleLoan->at(sfPrincipalOutstanding) = Number(100);
sleLoan->at(sfTotalValueOutstanding) = Number(150);
sleLoan->at(sfManagementFeeOutstanding) = Number(0);
sleLoan->at(sfPeriodicPayment) = Number(1);
sleLoan->setFieldU32(sfPaymentRemaining, 1);
ov.rawInsert(sleLoan);
}
STTx const tx{
ttLOAN_PAY, [](STObject& t) { t.setFieldAmount(sfAmount, XRPAmount(100)); }};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
// Cash received: assets available and the vault balance both grow by
// 60, paid by a2. The paid loan's claim drops by 60 (total value
// 150 -> 90). Conservation requires assets outstanding to be
// unchanged, but we bump it by 10 to violate the identity.
if (!BEAST_EXPECT(kAdjust(
ac.view(),
vaultKeylet,
Adjustments{
.assetsTotal = 10,
.assetsAvailable = 60,
.vaultAssets = 60,
.accountAssets = AccountAmount{.account = a2.id(), .amount = -60}})))
return;
auto sleLoan = ac.view().peek(loanKeylet);
if (!BEAST_EXPECT(sleLoan))
return;
sleLoan->at(sfPrincipalOutstanding) = Number(40);
sleLoan->at(sfTotalValueOutstanding) = Number(90);
ac.view().update(sleLoan);
auto transactor = makeTransactor(ac);
if (!BEAST_EXPECT(transactor))
return;
TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
BEAST_EXPECT(result == tecINVARIANT_FAILED);
BEAST_EXPECT(sink.messages().str().contains(
"loan pay assets outstanding must match the cash received and "
"the change in the loan claim"));
}
// ttVAULT_SET: owner is immutable
doInvariantCheck(
{"violation of vault immutable data"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
if (!sleVault)
return false;
sleVault->setAccountID(sfOwner, a2.id());
ac.view().update(sleVault);
return true;
},
XRPAmount{},
STTx{ttVAULT_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttVAULT_SET: withdrawal policy is immutable
doInvariantCheck(
{"violation of vault immutable data"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
if (!sleVault)
return false;
sleVault->setFieldU8(
sfWithdrawalPolicy,
static_cast<std::uint8_t>(sleVault->getFieldU8(sfWithdrawalPolicy) + 1));
ac.view().update(sleVault);
return true;
},
XRPAmount{},
STTx{ttVAULT_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
// ttVAULT_SET: scale is immutable
doInvariantCheck(
{"violation of vault immutable data"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
if (!sleVault)
return false;
sleVault->setFieldU8(
sfScale, static_cast<std::uint8_t>(sleVault->getFieldU8(sfScale) + 1));
ac.view().update(sleVault);
return true;
},
XRPAmount{},
STTx{ttVAULT_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp);
testcase << "Vault create";
doInvariantCheck(
{

View File

@@ -28,7 +28,6 @@
#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>
@@ -453,7 +452,7 @@ struct TestPeerSet : public PeerSet
dropRate = 100;
}
if (randInt(1, 100) <= dropRate)
if (((rand() % 100) + 1) <= dropRate)
return;
switch (type)

View File

@@ -0,0 +1,268 @@
#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

@@ -0,0 +1,64 @@
#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

@@ -0,0 +1,276 @@
#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

@@ -0,0 +1,879 @@
#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

@@ -0,0 +1,82 @@
#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

@@ -0,0 +1,309 @@
#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

@@ -0,0 +1,251 @@
#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

@@ -0,0 +1,344 @@
#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

@@ -0,0 +1,326 @@
#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

@@ -0,0 +1,440 @@
#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

@@ -0,0 +1,376 @@
#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,8 +1,6 @@
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/hash/hash_append.h>
#include <gtest/gtest.h>
#include <xrpl/beast/unit_test/suite.h>
#include <array>
#include <cstddef>
@@ -155,20 +153,20 @@ static_assert(sha256_t::kBits == 256, "sha256_t must have 256 bits");
namespace xrpl {
class HardenedHashTest : public ::testing::Test
class hardened_hash_test : public beast::unit_test::Suite
{
public:
template <class T>
static void
void
check()
{
T t{};
HardenedHash<>()(t);
SUCCEED();
pass();
}
template <template <class T> class U>
static void
void
checkUserType()
{
check<U<bool>>();
@@ -193,35 +191,48 @@ public:
}
template <template <class T> class C>
static void
void
checkContainer()
{
{
C<detail::TestUserTypeMember<std::string>> const c;
}
SUCCEED();
pass();
{
C<detail::TestUserTypeFree<std::string>> const c;
}
SUCCEED();
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();
}
};
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>();
}
BEAST_DEFINE_TESTSUITE(hardened_hash, basics, xrpl);
} // namespace xrpl

View File

@@ -0,0 +1,83 @@
#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

@@ -0,0 +1,296 @@
#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,7 +7,6 @@
#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>
@@ -17,7 +16,7 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <optional>
#include <string>
@@ -174,7 +173,7 @@ public:
Oracle const oracle(
env,
{.owner = owner,
.documentID = randInt<std::uint32_t>(),
.documentID = rand(),
.series = {{"XRP", "USD", 740 + i, 1}, {"XRP", "EUR", 740, 1}},
.fee = baseFee});
oracles.emplace_back(owner, oracle.documentID());

View File

@@ -0,0 +1,177 @@
#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

@@ -0,0 +1,427 @@
#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

@@ -4,6 +4,7 @@
#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>
@@ -91,17 +92,13 @@ public:
}
void
missingNodeAcquireBySeq(
[[maybe_unused]] std::uint32_t refNum,
[[maybe_unused]] uint256 const& nodeHash) override
missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) override
{
Throw<std::runtime_error>("missing node");
}
void
missingNodeAcquireByHash(
[[maybe_unused]] uint256 const& refHash,
[[maybe_unused]] std::uint32_t refNum) override
missingNodeAcquireByHash(uint256 const& refHash, std::uint32_t refNum) override
{
Throw<std::runtime_error>("missing node");
}
@@ -113,7 +110,7 @@ public:
(*tnCache_).reset();
}
TestStopwatch&
beast::ManualClock<std::chrono::steady_clock>
clock()
{
return clock_;

View File

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

View File

@@ -1,260 +0,0 @@
#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

@@ -1,94 +0,0 @@
#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

@@ -1,243 +0,0 @@
#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

@@ -1,844 +0,0 @@
#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

@@ -1,81 +0,0 @@
#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

@@ -1,293 +0,0 @@
#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

@@ -1,246 +0,0 @@
#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

@@ -1,328 +0,0 @@
#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

@@ -1,295 +0,0 @@
#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

@@ -1,438 +0,0 @@
#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

@@ -1,360 +0,0 @@
#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,80 +0,0 @@
#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

@@ -1,241 +0,0 @@
#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

@@ -1,22 +0,0 @@
#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

@@ -1,349 +0,0 @@
#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

@@ -1,170 +0,0 @@
#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/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<std::pair<SHAMapNodeID, Blob>> a;
ASSERT_TRUE(source.getNodeFat(SHAMapNodeID(), a, randBool(eng_), randInt(eng_, 2)));
ASSERT_FALSE(a.empty()) << "NodeSize";
ASSERT_TRUE(
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)
{
// 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
if (!destination.addKnownNode(i.first, makeSlice(i.second), nullptr).isUseful())
FAIL() << "Known node was not useful";
}
} while (true);
destination.clearSynching();
EXPECT_TRUE(source.deepCompare(destination));
destination.invariants();
}
} // namespace xrpl::tests