mirror of
https://github.com/XRPLF/rippled.git
synced 2026-01-22 07:35:27 +00:00
Compare commits
19 Commits
ximinez/va
...
ximinez/nu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f0690f3c5 | ||
|
|
040bf34257 | ||
|
|
5c87c4ffb0 | ||
|
|
0a9436def4 | ||
|
|
e5646e4ebe | ||
|
|
5e808794d8 | ||
|
|
f76bf5340c | ||
|
|
1af0f4bd43 | ||
|
|
c6821ab842 | ||
|
|
7ab9709373 | ||
|
|
aa12210fcd | ||
|
|
9235ec483a | ||
|
|
12c0d67ff6 | ||
|
|
00d3cee6cc | ||
|
|
ffe0a3cc61 | ||
|
|
add9071b20 | ||
|
|
465e7b6d91 | ||
|
|
6223ebe05e | ||
|
|
4fe50c2d31 |
@@ -104,6 +104,7 @@ test.overlay > xrpl.basics
|
||||
test.overlay > xrpld.app
|
||||
test.overlay > xrpld.overlay
|
||||
test.overlay > xrpld.peerfinder
|
||||
test.overlay > xrpl.nodestore
|
||||
test.overlay > xrpl.protocol
|
||||
test.overlay > xrpl.shamap
|
||||
test.peerfinder > test.beast
|
||||
|
||||
4
.github/scripts/strategy-matrix/generate.py
vendored
4
.github/scripts/strategy-matrix/generate.py
vendored
@@ -20,8 +20,8 @@ class Config:
|
||||
Generate a strategy matrix for GitHub Actions CI.
|
||||
|
||||
On each PR commit we will build a selection of Debian, RHEL, Ubuntu, MacOS, and
|
||||
Windows configurations, while upon merge into the develop, release, or master
|
||||
branches, we will build all configurations, and test most of them.
|
||||
Windows configurations, while upon merge into the develop or release branches,
|
||||
we will build all configurations, and test most of them.
|
||||
|
||||
We will further set additional CMake arguments as follows:
|
||||
- All builds will have the `tests`, `werr`, and `xrpld` options.
|
||||
|
||||
2
.github/workflows/on-pr.yml
vendored
2
.github/workflows/on-pr.yml
vendored
@@ -125,7 +125,7 @@ jobs:
|
||||
needs:
|
||||
- should-run
|
||||
- build-test
|
||||
if: ${{ needs.should-run.outputs.go == 'true' && (startsWith(github.base_ref, 'release') || github.base_ref == 'master') }}
|
||||
if: ${{ needs.should-run.outputs.go == 'true' && startsWith(github.ref, 'refs/heads/release') }}
|
||||
uses: ./.github/workflows/reusable-notify-clio.yml
|
||||
secrets:
|
||||
clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }}
|
||||
|
||||
20
.github/workflows/on-trigger.yml
vendored
20
.github/workflows/on-trigger.yml
vendored
@@ -1,9 +1,8 @@
|
||||
# This workflow runs all workflows to build the dependencies required for the
|
||||
# project on various Linux flavors, as well as on MacOS and Windows, on a
|
||||
# scheduled basis, on merge into the 'develop', 'release', or 'master' branches,
|
||||
# or manually. The missing commits check is only run when the code is merged
|
||||
# into the 'develop' or 'release' branches, and the documentation is built when
|
||||
# the code is merged into the 'develop' branch.
|
||||
# This workflow runs all workflows to build and test the code on various Linux
|
||||
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
|
||||
# the 'develop' or 'release*' branches, or when requested manually. Upon
|
||||
# successful completion, it also uploads the built libxrpl package to the Conan
|
||||
# remote.
|
||||
name: Trigger
|
||||
|
||||
on:
|
||||
@@ -11,7 +10,6 @@ on:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
- "master"
|
||||
paths:
|
||||
# These paths are unique to `on-trigger.yml`.
|
||||
- ".github/workflows/on-trigger.yml"
|
||||
@@ -70,10 +68,10 @@ jobs:
|
||||
with:
|
||||
# Enable ccache only for events targeting the XRPLF repository, since
|
||||
# other accounts will not have access to our remote cache storage.
|
||||
# However, we do not enable ccache for events targeting the master or a
|
||||
# release branch, to protect against the rare case that the output
|
||||
# produced by ccache is not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !(github.base_ref == 'master' || startsWith(github.base_ref, 'release')) }}
|
||||
# However, we do not enable ccache for events targeting a release branch,
|
||||
# to protect against the rare case that the output produced by ccache is
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
os: ${{ matrix.os }}
|
||||
strategy_matrix: ${{ github.event_name == 'schedule' && 'all' || 'minimal' }}
|
||||
secrets:
|
||||
|
||||
4
.github/workflows/pre-commit.yml
vendored
4
.github/workflows/pre-commit.yml
vendored
@@ -3,7 +3,9 @@ name: Run pre-commit hooks
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [develop, release, master]
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -109,6 +109,10 @@ template <class T>
|
||||
concept Integral64 =
|
||||
std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>;
|
||||
|
||||
template <class STAmount, class Asset>
|
||||
concept CanUseAsScale = requires(Asset a, Number n) { STAmount(a, n); } &&
|
||||
requires(STAmount s) { s.exponent(); };
|
||||
|
||||
/** Number is a floating point type that can represent a wide range of values.
|
||||
*
|
||||
* It can represent all values that can be represented by an STAmount -
|
||||
@@ -268,6 +272,26 @@ public:
|
||||
constexpr int
|
||||
exponent() const noexcept;
|
||||
|
||||
/** Get the scale of this Number for the given asset.
|
||||
*
|
||||
* "scale" is similar to "exponent", but from the perspective of STAmount,
|
||||
* which has different rules for determining the exponent than Number.
|
||||
*
|
||||
* Because Number does not have access to STAmount or Asset, this function
|
||||
* is implemented as a template, with the expectation that it will only be
|
||||
* used by those types. Any types that fit the requirements will work,
|
||||
* though, if there's a need.
|
||||
*
|
||||
* @tparam STAmount The STAmount type.
|
||||
* @tparam Asset The Asset type.
|
||||
* @param asset The asset to use for determining the scale.
|
||||
* @return The scale of this Number for the given asset.
|
||||
*/
|
||||
template <class STAmount, class Asset>
|
||||
int
|
||||
scale(Asset const& asset) const
|
||||
requires CanUseAsScale<STAmount, Asset>;
|
||||
|
||||
constexpr Number
|
||||
operator+() const noexcept;
|
||||
constexpr Number
|
||||
@@ -602,6 +626,14 @@ Number::exponent() const noexcept
|
||||
return e;
|
||||
}
|
||||
|
||||
template <class STAmount, class Asset>
|
||||
int
|
||||
Number::scale(Asset const& asset) const
|
||||
requires CanUseAsScale<STAmount, Asset>
|
||||
{
|
||||
return STAmount{asset, *this}.exponent();
|
||||
}
|
||||
|
||||
inline constexpr Number
|
||||
Number::operator+() const noexcept
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/ApplyContext.h>
|
||||
#include <xrpld/app/tx/detail/InvariantCheck.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -20,6 +21,9 @@
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
|
||||
@@ -3888,6 +3892,140 @@ class Invariants_test : public beast::unit_test::suite
|
||||
precloseMpt);
|
||||
}
|
||||
|
||||
void
|
||||
testVaultComputeMinScale()
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const vaultAsset = issuer["IOU"];
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
std::int32_t expectedMinScale;
|
||||
std::initializer_list<ValidVault::DeltaInfo const> values;
|
||||
};
|
||||
|
||||
NumberMantissaScaleGuard g{MantissaRange::large};
|
||||
|
||||
auto makeDelta =
|
||||
[&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
|
||||
return {n, n.scale<STAmount>(vaultAsset.raw())};
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "No values",
|
||||
.expectedMinScale = 0,
|
||||
.values = {},
|
||||
},
|
||||
{
|
||||
.name = "Mixed integer and Number values",
|
||||
.expectedMinScale = -15,
|
||||
.values =
|
||||
{makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
|
||||
},
|
||||
{
|
||||
.name = "Mixed scales",
|
||||
.expectedMinScale = -17,
|
||||
.values =
|
||||
{makeDelta(Number{1, -2}),
|
||||
makeDelta(Number{5, -3}),
|
||||
makeDelta(Number{3, -2})},
|
||||
},
|
||||
{
|
||||
.name = "Equal scales",
|
||||
.expectedMinScale = -16,
|
||||
.values =
|
||||
{makeDelta(Number{1, -1}),
|
||||
makeDelta(Number{5, -1}),
|
||||
makeDelta(Number{1, -1})},
|
||||
},
|
||||
{
|
||||
.name = "Mixed mantissa sizes",
|
||||
.expectedMinScale = -12,
|
||||
.values =
|
||||
{makeDelta(Number{1}),
|
||||
makeDelta(Number{1234, -3}),
|
||||
makeDelta(Number{12345, -6}),
|
||||
makeDelta(Number{123, 1})},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("vault computeMinScale: " + tc.name);
|
||||
|
||||
auto const actualScale =
|
||||
ValidVault::computeMinScale(vaultAsset, tc.values);
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualScale == tc.expectedMinScale,
|
||||
"expected: " + std::to_string(tc.expectedMinScale) +
|
||||
", actual: " + std::to_string(actualScale));
|
||||
for (auto const& num : tc.values)
|
||||
{
|
||||
// None of these scales are far enough apart that rounding the
|
||||
// values would lose information, so check that the rounded
|
||||
// value matches the original.
|
||||
auto const actualRounded =
|
||||
roundToAsset(vaultAsset, num.delta, actualScale);
|
||||
BEAST_EXPECTS(
|
||||
actualRounded == num.delta,
|
||||
"number " + to_string(num.delta) + " rounded to scale " +
|
||||
std::to_string(actualScale) + " is " +
|
||||
to_string(actualRounded));
|
||||
}
|
||||
}
|
||||
|
||||
auto const testCases2 = std::vector<TestCase>{
|
||||
{
|
||||
.name = "False equivalence",
|
||||
.expectedMinScale = -15,
|
||||
.values =
|
||||
{
|
||||
makeDelta(Number{1234567890123456789, -18}),
|
||||
makeDelta(Number{12345, -4}),
|
||||
makeDelta(Number{1}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Unlike the first set of test cases, the values in these test could
|
||||
// look equivalent if using the wrong scale.
|
||||
for (auto const& tc : testCases2)
|
||||
{
|
||||
testcase("vault computeMinScale: " + tc.name);
|
||||
|
||||
auto const actualScale =
|
||||
ValidVault::computeMinScale(vaultAsset, tc.values);
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualScale == tc.expectedMinScale,
|
||||
"expected: " + std::to_string(tc.expectedMinScale) +
|
||||
", actual: " + std::to_string(actualScale));
|
||||
std::optional<Number> first;
|
||||
Number firstRounded;
|
||||
for (auto const& num : tc.values)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
first = num.delta;
|
||||
firstRounded =
|
||||
roundToAsset(vaultAsset, num.delta, actualScale);
|
||||
continue;
|
||||
}
|
||||
auto const numRounded =
|
||||
roundToAsset(vaultAsset, num.delta, actualScale);
|
||||
BEAST_EXPECTS(
|
||||
numRounded != firstRounded,
|
||||
"at a scale of " + std::to_string(actualScale) + " " +
|
||||
to_string(num.delta) + " == " + to_string(*first));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -3911,6 +4049,7 @@ public:
|
||||
testValidPseudoAccounts();
|
||||
testValidLoanBroker();
|
||||
testVault();
|
||||
testVaultComputeMinScale();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,16 +3,11 @@
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpld/app/misc/LendingHelpers.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpld/app/tx/detail/Batch.h>
|
||||
#include <xrpld/app/tx/detail/LoanSet.h>
|
||||
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -7641,6 +7641,149 @@ protected:
|
||||
BEAST_EXPECT(afterSecondCoverAvailable == 0);
|
||||
}
|
||||
|
||||
// Tests that vault withdrawals work correctly when the vault has unrealized
|
||||
// loss from an impaired loan, ensuring the invariant check properly
|
||||
// accounts for the loss.
|
||||
void
|
||||
testWithdrawReflectsUnrealizedLoss()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
testcase("Vault withdraw reflects sfLossUnrealized");
|
||||
|
||||
// Test constants
|
||||
static constexpr std::int64_t INITIAL_FUNDING = 1'000'000;
|
||||
static constexpr std::int64_t LENDER_INITIAL_IOU = 5'000'000;
|
||||
static constexpr std::int64_t DEPOSITOR_INITIAL_IOU = 1'000'000;
|
||||
static constexpr std::int64_t BORROWER_INITIAL_IOU = 100'000;
|
||||
static constexpr std::int64_t DEPOSIT_AMOUNT = 5'000;
|
||||
static constexpr std::int64_t PRINCIPAL_AMOUNT = 99;
|
||||
static constexpr std::uint64_t EXPECTED_SHARES_PER_DEPOSITOR =
|
||||
5'000'000'000;
|
||||
static constexpr std::uint32_t PAYMENT_INTERVAL = 600;
|
||||
static constexpr std::uint32_t PAYMENT_TOTAL = 2;
|
||||
|
||||
Env env(*this, all);
|
||||
|
||||
// Setup accounts
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"lender"};
|
||||
Account const depositorA{"lpA"};
|
||||
Account const depositorB{"lpB"};
|
||||
Account const borrower{"borrowerA"};
|
||||
|
||||
env.fund(
|
||||
XRP(INITIAL_FUNDING),
|
||||
issuer,
|
||||
lender,
|
||||
depositorA,
|
||||
depositorB,
|
||||
borrower);
|
||||
env.close();
|
||||
|
||||
// Setup trust lines
|
||||
PrettyAsset const iouAsset = issuer[iouCurrency];
|
||||
env(trust(lender, iouAsset(10'000'000)));
|
||||
env(trust(depositorA, iouAsset(10'000'000)));
|
||||
env(trust(depositorB, iouAsset(10'000'000)));
|
||||
env(trust(borrower, iouAsset(10'000'000)));
|
||||
env.close();
|
||||
|
||||
// Fund accounts with IOUs
|
||||
env(pay(issuer, lender, iouAsset(LENDER_INITIAL_IOU)));
|
||||
env(pay(issuer, depositorA, iouAsset(DEPOSITOR_INITIAL_IOU)));
|
||||
env(pay(issuer, depositorB, iouAsset(DEPOSITOR_INITIAL_IOU)));
|
||||
env(pay(issuer, borrower, iouAsset(BORROWER_INITIAL_IOU)));
|
||||
env.close();
|
||||
|
||||
// Create vault and broker, then add deposits from two depositors
|
||||
auto const broker = createVaultAndBroker(env, iouAsset, lender);
|
||||
Vault v{env};
|
||||
|
||||
env(v.deposit({
|
||||
.depositor = depositorA,
|
||||
.id = broker.vaultKeylet().key,
|
||||
.amount = iouAsset(DEPOSIT_AMOUNT),
|
||||
}),
|
||||
ter(tesSUCCESS));
|
||||
env(v.deposit({
|
||||
.depositor = depositorB,
|
||||
.id = broker.vaultKeylet().key,
|
||||
.amount = iouAsset(DEPOSIT_AMOUNT),
|
||||
}),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Create a loan
|
||||
auto const sleBroker = env.le(keylet::loanbroker(broker.brokerID));
|
||||
if (!BEAST_EXPECT(sleBroker))
|
||||
return;
|
||||
|
||||
auto const loanKeylet =
|
||||
keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence));
|
||||
|
||||
env(set(borrower, broker.brokerID, PRINCIPAL_AMOUNT),
|
||||
sig(sfCounterpartySignature, lender),
|
||||
paymentTotal(PAYMENT_TOTAL),
|
||||
paymentInterval(PAYMENT_INTERVAL),
|
||||
fee(env.current()->fees().base * 2),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Impair the loan to create unrealized loss
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Verify unrealized loss is recorded in the vault
|
||||
auto const vaultAfterImpair = env.le(broker.vaultKeylet());
|
||||
if (!BEAST_EXPECT(vaultAfterImpair))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(
|
||||
vaultAfterImpair->at(sfLossUnrealized) ==
|
||||
broker.asset(PRINCIPAL_AMOUNT).value());
|
||||
|
||||
// Helper to get share balance for a depositor
|
||||
auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
|
||||
auto const getShareBalance =
|
||||
[&](Account const& depositor) -> std::uint64_t {
|
||||
auto const token =
|
||||
env.le(keylet::mptoken(shareAsset, depositor.id()));
|
||||
return token ? token->getFieldU64(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// Verify both depositors have equal shares
|
||||
auto const sharesLpA = getShareBalance(depositorA);
|
||||
auto const sharesLpB = getShareBalance(depositorB);
|
||||
BEAST_EXPECT(sharesLpA == EXPECTED_SHARES_PER_DEPOSITOR);
|
||||
BEAST_EXPECT(sharesLpB == EXPECTED_SHARES_PER_DEPOSITOR);
|
||||
BEAST_EXPECT(sharesLpA == sharesLpB);
|
||||
|
||||
// Helper to attempt withdrawal
|
||||
auto const attemptWithdrawShares = [&](Account const& depositor,
|
||||
std::uint64_t shareAmount,
|
||||
TER expected) {
|
||||
STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)};
|
||||
env(v.withdraw(
|
||||
{.depositor = depositor,
|
||||
.id = broker.vaultKeylet().key,
|
||||
.amount = shareAmt}),
|
||||
ter(expected));
|
||||
env.close();
|
||||
};
|
||||
|
||||
// Regression test: Both depositors should successfully withdraw despite
|
||||
// unrealized loss. Previously failed with invariant violation:
|
||||
// "withdrawal must change vault and destination balance by equal
|
||||
// amount". This was caused by sharesToAssetsWithdraw rounding down,
|
||||
// creating a mismatch where vaultDeltaAssets * -1 != destinationDelta
|
||||
// when unrealized loss exists.
|
||||
attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS);
|
||||
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -7649,6 +7792,7 @@ public:
|
||||
testLoanPayLateFullPaymentBypassesPenalties();
|
||||
testLoanCoverMinimumRoundingExploit();
|
||||
#endif
|
||||
testWithdrawReflectsUnrealizedLoss();
|
||||
testInvalidLoanSet();
|
||||
|
||||
testCoverDepositWithdrawNonTransferableMPT();
|
||||
|
||||
211
src/test/overlay/TMGetObjectByHash_test.cpp
Normal file
211
src/test/overlay/TMGetObjectByHash_test.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/overlay/Message.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/overlay/detail/Tuning.h>
|
||||
#include <xrpld/peerfinder/detail/SlotImp.h>
|
||||
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/nodestore/NodeObject.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
/**
|
||||
* Test for TMGetObjectByHash reply size limiting.
|
||||
*
|
||||
* This verifies the fix that limits TMGetObjectByHash replies to
|
||||
* Tuning::hardMaxReplyNodes to prevent excessive memory usage and
|
||||
* potential DoS attacks from peers requesting large numbers of objects.
|
||||
*/
|
||||
class TMGetObjectByHash_test : public beast::unit_test::suite
|
||||
{
|
||||
using middle_type = boost::beast::tcp_stream;
|
||||
using stream_type = boost::beast::ssl_stream<middle_type>;
|
||||
using socket_type = boost::asio::ip::tcp::socket;
|
||||
using shared_context = std::shared_ptr<boost::asio::ssl::context>;
|
||||
/**
|
||||
* Test peer that captures sent messages for verification.
|
||||
*/
|
||||
class PeerTest : public PeerImp
|
||||
{
|
||||
public:
|
||||
PeerTest(
|
||||
Application& app,
|
||||
std::shared_ptr<PeerFinder::Slot> const& slot,
|
||||
http_request_type&& request,
|
||||
PublicKey const& publicKey,
|
||||
ProtocolVersion protocol,
|
||||
Resource::Consumer consumer,
|
||||
std::unique_ptr<TMGetObjectByHash_test::stream_type>&& stream_ptr,
|
||||
OverlayImpl& overlay)
|
||||
: PeerImp(
|
||||
app,
|
||||
id_++,
|
||||
slot,
|
||||
std::move(request),
|
||||
publicKey,
|
||||
protocol,
|
||||
consumer,
|
||||
std::move(stream_ptr),
|
||||
overlay)
|
||||
{
|
||||
}
|
||||
|
||||
~PeerTest() = default;
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
send(std::shared_ptr<Message> const& m) override
|
||||
{
|
||||
lastSentMessage_ = m;
|
||||
}
|
||||
|
||||
std::shared_ptr<Message>
|
||||
getLastSentMessage() const
|
||||
{
|
||||
return lastSentMessage_;
|
||||
}
|
||||
|
||||
static void
|
||||
resetId()
|
||||
{
|
||||
id_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
inline static Peer::id_t id_ = 0;
|
||||
std::shared_ptr<Message> lastSentMessage_;
|
||||
};
|
||||
|
||||
shared_context context_{make_SSLContext("")};
|
||||
ProtocolVersion protocolVersion_{1, 7};
|
||||
|
||||
std::shared_ptr<PeerTest>
|
||||
createPeer(jtx::Env& env)
|
||||
{
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
boost::beast::http::request<boost::beast::http::dynamic_body> request;
|
||||
auto stream_ptr = std::make_unique<stream_type>(
|
||||
socket_type(env.app().getIOContext()), *context_);
|
||||
|
||||
beast::IP::Endpoint local(
|
||||
boost::asio::ip::make_address("172.1.1.1"), 51235);
|
||||
beast::IP::Endpoint remote(
|
||||
boost::asio::ip::make_address("172.1.1.2"), 51235);
|
||||
|
||||
PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519)));
|
||||
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
|
||||
auto [slot, _] = overlay.peerFinder().new_inbound_slot(local, remote);
|
||||
|
||||
auto peer = std::make_shared<PeerTest>(
|
||||
env.app(),
|
||||
slot,
|
||||
std::move(request),
|
||||
key,
|
||||
protocolVersion_,
|
||||
consumer,
|
||||
std::move(stream_ptr),
|
||||
overlay);
|
||||
|
||||
overlay.add_active(peer);
|
||||
return peer;
|
||||
}
|
||||
|
||||
std::shared_ptr<protocol::TMGetObjectByHash>
|
||||
createRequest(size_t const numObjects, Env& env)
|
||||
{
|
||||
// Store objects in the NodeStore that will be found during the query
|
||||
auto& nodeStore = env.app().getNodeStore();
|
||||
|
||||
// Create and store objects
|
||||
std::vector<uint256> hashes;
|
||||
hashes.reserve(numObjects);
|
||||
for (int i = 0; i < numObjects; ++i)
|
||||
{
|
||||
uint256 hash(xrpl::sha512Half(i));
|
||||
hashes.push_back(hash);
|
||||
|
||||
Blob data(100, static_cast<unsigned char>(i % 256));
|
||||
nodeStore.store(
|
||||
hotLEDGER,
|
||||
std::move(data),
|
||||
hash,
|
||||
nodeStore.earliestLedgerSeq());
|
||||
}
|
||||
|
||||
// Create a request with more objects than hardMaxReplyNodes
|
||||
auto request = std::make_shared<protocol::TMGetObjectByHash>();
|
||||
request->set_type(protocol::TMGetObjectByHash_ObjectType_otLEDGER);
|
||||
request->set_query(true);
|
||||
|
||||
for (int i = 0; i < numObjects; ++i)
|
||||
{
|
||||
auto object = request->add_objects();
|
||||
object->set_hash(hashes[i].data(), hashes[i].size());
|
||||
object->set_ledgerseq(i);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that reply is limited to hardMaxReplyNodes when more objects
|
||||
* are requested than the limit allows.
|
||||
*/
|
||||
void
|
||||
testReplyLimit(size_t const numObjects, int const expectedReplySize)
|
||||
{
|
||||
testcase("Reply Limit");
|
||||
|
||||
Env env(*this);
|
||||
PeerTest::resetId();
|
||||
|
||||
auto peer = createPeer(env);
|
||||
|
||||
auto request = createRequest(numObjects, env);
|
||||
// Call the onMessage handler
|
||||
peer->onMessage(request);
|
||||
|
||||
// Verify that a reply was sent
|
||||
auto sentMessage = peer->getLastSentMessage();
|
||||
BEAST_EXPECT(sentMessage != nullptr);
|
||||
|
||||
// Parse the reply message
|
||||
auto const& buffer =
|
||||
sentMessage->getBuffer(compression::Compressed::Off);
|
||||
|
||||
BEAST_EXPECT(buffer.size() > 6);
|
||||
// Skip the message header (6 bytes: 4 for size, 2 for type)
|
||||
protocol::TMGetObjectByHash reply;
|
||||
BEAST_EXPECT(
|
||||
reply.ParseFromArray(buffer.data() + 6, buffer.size() - 6) == true);
|
||||
|
||||
// Verify the reply is limited to expectedReplySize
|
||||
BEAST_EXPECT(reply.objects_size() == expectedReplySize);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
int const limit = static_cast<int>(Tuning::hardMaxReplyNodes);
|
||||
testReplyLimit(limit + 1, limit);
|
||||
testReplyLimit(limit, limit);
|
||||
testReplyLimit(limit - 1, limit - 1);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(TMGetObjectByHash, overlay, xrpl);
|
||||
|
||||
} // namespace test
|
||||
} // namespace xrpl
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/xchain_bridge.h>
|
||||
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -30,6 +32,7 @@ enum class FieldType {
|
||||
CurrencyField,
|
||||
HashField,
|
||||
HashOrObjectField,
|
||||
FixedHashField,
|
||||
IssueField,
|
||||
ObjectField,
|
||||
StringField,
|
||||
@@ -86,6 +89,7 @@ getTypeName(FieldType typeID)
|
||||
case FieldType::CurrencyField:
|
||||
return "Currency";
|
||||
case FieldType::HashField:
|
||||
case FieldType::FixedHashField:
|
||||
return "hex string";
|
||||
case FieldType::HashOrObjectField:
|
||||
return "hex string or object";
|
||||
@@ -202,6 +206,7 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
static auto const& badBlobValues = remove({3, 7, 8, 16});
|
||||
static auto const& badCurrencyValues = remove({14});
|
||||
static auto const& badHashValues = remove({2, 3, 7, 8, 16});
|
||||
static auto const& badFixedHashValues = remove({1, 2, 3, 4, 7, 8, 16});
|
||||
static auto const& badIndexValues = remove({12, 16, 18, 19});
|
||||
static auto const& badUInt32Values = remove({2, 3});
|
||||
static auto const& badUInt64Values = remove({2, 3});
|
||||
@@ -222,6 +227,8 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
return badHashValues;
|
||||
case FieldType::HashOrObjectField:
|
||||
return badIndexValues;
|
||||
case FieldType::FixedHashField:
|
||||
return badFixedHashValues;
|
||||
case FieldType::IssueField:
|
||||
return badIssueValues;
|
||||
case FieldType::UInt32Field:
|
||||
@@ -717,7 +724,12 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
// negative tests
|
||||
runLedgerEntryTest(env, jss::amendments);
|
||||
testMalformedField(
|
||||
env,
|
||||
Json::Value{},
|
||||
jss::amendments,
|
||||
FieldType::FixedHashField,
|
||||
"malformedRequest");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1538,7 +1550,12 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
// negative tests
|
||||
runLedgerEntryTest(env, jss::fee);
|
||||
testMalformedField(
|
||||
env,
|
||||
Json::Value{},
|
||||
jss::fee,
|
||||
FieldType::FixedHashField,
|
||||
"malformedRequest");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1561,7 +1578,12 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
// negative tests
|
||||
runLedgerEntryTest(env, jss::hashes);
|
||||
testMalformedField(
|
||||
env,
|
||||
Json::Value{},
|
||||
jss::hashes,
|
||||
FieldType::FixedHashField,
|
||||
"malformedRequest");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1686,7 +1708,12 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
// negative tests
|
||||
runLedgerEntryTest(env, jss::nunl);
|
||||
testMalformedField(
|
||||
env,
|
||||
Json::Value{},
|
||||
jss::nunl,
|
||||
FieldType::FixedHashField,
|
||||
"malformedRequest");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2343,6 +2370,438 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the ledger entry types that don't take parameters
|
||||
void
|
||||
testFixed()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
Env env{*this, envconfig([](auto cfg) {
|
||||
cfg->START_UP = Config::FRESH;
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
env.close();
|
||||
|
||||
/** Verifies that the RPC result has the expected data
|
||||
*
|
||||
* @param good: Indicates that the request should have succeeded
|
||||
* and returned a ledger object of `expectedType` type.
|
||||
* @param jv: The RPC result Json value
|
||||
* @param expectedType: The type that the ledger object should
|
||||
* have if "good".
|
||||
* @param expectedError: Optional. The expected error if not
|
||||
* good. Defaults to "entryNotFound".
|
||||
*/
|
||||
auto checkResult =
|
||||
[&](bool good,
|
||||
Json::Value const& jv,
|
||||
Json::StaticString const& expectedType,
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
!jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember(
|
||||
sfLedgerEntryType.jsonName) &&
|
||||
jv[jss::result][jss::node]
|
||||
[sfLedgerEntryType.jsonName] == expectedType,
|
||||
to_string(jv));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] ==
|
||||
expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
|
||||
/** Runs a series of tests for a given fixed-position ledger
|
||||
* entry.
|
||||
*
|
||||
* @param field: The Json request field to use.
|
||||
* @param expectedType: The type that the ledger object should
|
||||
* have if "good".
|
||||
* @param expectedKey: The keylet of the fixed object.
|
||||
* @param good: Indicates whether the object is expected to
|
||||
* exist.
|
||||
*/
|
||||
auto test = [&](Json::StaticString const& field,
|
||||
Json::StaticString const& expectedType,
|
||||
Keylet const& expectedKey,
|
||||
bool good) {
|
||||
testcase << expectedType.c_str() << (good ? "" : " not")
|
||||
<< " found";
|
||||
|
||||
auto const hexKey = strHex(expectedKey.key);
|
||||
|
||||
{
|
||||
// Test bad values
|
||||
// "field":null
|
||||
Json::Value params;
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = Json::nullValue;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "field":"string"
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = "arbitrary string";
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "field":false
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = false;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "invalidParams");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
|
||||
// "field":[incorrect index hash]
|
||||
auto const badKey = strHex(expectedKey.key + uint256{1});
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = badKey;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "entryNotFound");
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::result][jss::index] == badKey, to_string(jv));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "index":"field" using API 2
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 2;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
std::string const pdIdx = [&]() {
|
||||
{
|
||||
Json::Value params;
|
||||
// Test good values
|
||||
// Use the "field":true notation
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = true;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index will always be returned for valid parameters.
|
||||
std::string const pdIdx =
|
||||
jv[jss::result][jss::index].asString();
|
||||
BEAST_EXPECTS(hexKey == pdIdx, to_string(jv));
|
||||
checkResult(good, jv, expectedType);
|
||||
|
||||
return pdIdx;
|
||||
}
|
||||
}();
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "field":"[index hash]"
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = hexKey;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedType);
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
}
|
||||
|
||||
{
|
||||
// Bad value
|
||||
// Use the "index":"field" notation with API 2
|
||||
Json::Value params;
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 2;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// Use the "index":"field" notation with API 3
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 3;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index is correct either way
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
checkResult(good, jv, expectedType);
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// Use the "index":"[index hash]" notation
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = pdIdx;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index is correct either way
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
checkResult(good, jv, expectedType);
|
||||
}
|
||||
};
|
||||
|
||||
test(jss::amendments, jss::Amendments, keylet::amendments(), true);
|
||||
test(jss::fee, jss::FeeSettings, keylet::fees(), true);
|
||||
// There won't be an nunl
|
||||
test(jss::nunl, jss::NegativeUNL, keylet::negativeUNL(), false);
|
||||
// Can only get the short skip list this way
|
||||
test(jss::hashes, jss::LedgerHashes, keylet::skip(), true);
|
||||
}
|
||||
|
||||
void
|
||||
testHashes()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
Env env{*this, envconfig([](auto cfg) {
|
||||
cfg->START_UP = Config::FRESH;
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
env.close();
|
||||
|
||||
/** Verifies that the RPC result has the expected data
|
||||
*
|
||||
* @param good: Indicates that the request should have succeeded
|
||||
* and returned a ledger object of `expectedType` type.
|
||||
* @param jv: The RPC result Json value
|
||||
* @param expectedCount: The number of Hashes expected in the
|
||||
* object if "good".
|
||||
* @param expectedError: Optional. The expected error if not
|
||||
* good. Defaults to "entryNotFound".
|
||||
*/
|
||||
auto checkResult =
|
||||
[&](bool good,
|
||||
Json::Value const& jv,
|
||||
int expectedCount,
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
!jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember(
|
||||
sfLedgerEntryType.jsonName) &&
|
||||
jv[jss::result][jss::node]
|
||||
[sfLedgerEntryType.jsonName] == jss::LedgerHashes,
|
||||
to_string(jv));
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember("Hashes") &&
|
||||
jv[jss::result][jss::node]["Hashes"].size() ==
|
||||
expectedCount,
|
||||
to_string(jv[jss::result][jss::node]["Hashes"].size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] ==
|
||||
expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
|
||||
/** Runs a series of tests for a given ledger index.
|
||||
*
|
||||
* @param ledger: The ledger index value of the "hashes" request
|
||||
* parameter. May not necessarily be a number.
|
||||
* @param expectedKey: The expected keylet of the object.
|
||||
* @param good: Indicates whether the object is expected to
|
||||
* exist.
|
||||
* @param expectedCount: The number of Hashes expected in the
|
||||
* object if "good".
|
||||
*/
|
||||
auto test = [&](Json::Value ledger,
|
||||
Keylet const& expectedKey,
|
||||
bool good,
|
||||
int expectedCount = 0) {
|
||||
testcase << "LedgerHashes: seq: " << env.current()->header().seq
|
||||
<< " \"hashes\":" << to_string(ledger)
|
||||
<< (good ? "" : " not") << " found";
|
||||
|
||||
auto const hexKey = strHex(expectedKey.key);
|
||||
|
||||
{
|
||||
// Test bad values
|
||||
// "hashes":null
|
||||
Json::Value params;
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = Json::nullValue;
|
||||
auto jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "hashes":"non-uint string"
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = "arbitrary string";
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "hashes":"uint string" is invalid, too
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = "10";
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "hashes":false
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = false;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "invalidParams");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "hashes":-1
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = -1;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "internal");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
}
|
||||
|
||||
// "hashes":[incorrect index hash]
|
||||
{
|
||||
Json::Value params;
|
||||
auto const badKey = strHex(expectedKey.key + uint256{1});
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = badKey;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "entryNotFound");
|
||||
BEAST_EXPECT(jv[jss::result][jss::index] == badKey);
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// Test good values
|
||||
// Use the "hashes":ledger notation
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = ledger;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedCount);
|
||||
// Index will always be returned for valid parameters.
|
||||
std::string const pdIdx =
|
||||
jv[jss::result][jss::index].asString();
|
||||
BEAST_EXPECTS(hexKey == pdIdx, strHex(pdIdx));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// "hashes":"[index hash]"
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = hexKey;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedCount);
|
||||
// Index is correct either way
|
||||
BEAST_EXPECTS(
|
||||
hexKey == jv[jss::result][jss::index].asString(),
|
||||
strHex(jv[jss::result][jss::index].asString()));
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value params;
|
||||
// Use the "index":"[index hash]" notation
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = hexKey;
|
||||
auto const jv =
|
||||
env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedCount);
|
||||
// Index is correct either way
|
||||
BEAST_EXPECTS(
|
||||
hexKey == jv[jss::result][jss::index].asString(),
|
||||
strHex(jv[jss::result][jss::index].asString()));
|
||||
}
|
||||
};
|
||||
|
||||
// short skip list
|
||||
test(true, keylet::skip(), true, 2);
|
||||
// long skip list at index 0
|
||||
test(1, keylet::skip(1), false);
|
||||
// long skip list at index 1
|
||||
test(1 << 17, keylet::skip(1 << 17), false);
|
||||
|
||||
// Close more ledgers, but stop short of the flag ledger
|
||||
for (auto i = env.current()->seq(); i <= 250; ++i)
|
||||
env.close();
|
||||
|
||||
// short skip list
|
||||
test(true, keylet::skip(), true, 249);
|
||||
// long skip list at index 0
|
||||
test(1, keylet::skip(1), false);
|
||||
// long skip list at index 1
|
||||
test(1 << 17, keylet::skip(1 << 17), false);
|
||||
|
||||
// Close a flag ledger so the first "long" skip list is created
|
||||
for (auto i = env.current()->seq(); i <= 260; ++i)
|
||||
env.close();
|
||||
|
||||
// short skip list
|
||||
test(true, keylet::skip(), true, 256);
|
||||
// long skip list at index 0
|
||||
test(1, keylet::skip(1), true, 1);
|
||||
// long skip list at index 1
|
||||
test(1 << 17, keylet::skip(1 << 17), false);
|
||||
}
|
||||
|
||||
void
|
||||
testCLI()
|
||||
{
|
||||
@@ -2400,6 +2859,8 @@ public:
|
||||
testOracleLedgerEntry();
|
||||
testMPT();
|
||||
testPermissionedDomain();
|
||||
testFixed();
|
||||
testHashes();
|
||||
testCLI();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,8 +176,7 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::minExponent - 1; // LCOV_EXCL_LINE
|
||||
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}
|
||||
.exponent();
|
||||
return vaultSle->at(sfAssetsTotal).scale<STAmount>(vaultSle->at(sfAsset));
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
#include <xrpl/protocol/nftPageMask.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -2664,7 +2665,7 @@ ValidVault::visitEntry(
|
||||
// state (zero if created) and "after" state (zero if destroyed), so the
|
||||
// invariants can validate that the change in account balances matches the
|
||||
// change in vault balances, stored to deltas_ at the end of this function.
|
||||
Number balanceDelta{};
|
||||
DeltaInfo balanceDelta{numZero, STAmount::cMinOffset - 1};
|
||||
|
||||
std::int8_t sign = 0;
|
||||
if (before)
|
||||
@@ -2678,20 +2679,35 @@ ValidVault::visitEntry(
|
||||
// At this moment we have no way of telling if this object holds
|
||||
// vault shares or something else. Save it for finalize.
|
||||
beforeMPTs_.push_back(Shares::make(*before));
|
||||
balanceDelta = static_cast<std::int64_t>(
|
||||
balanceDelta.delta = static_cast<std::int64_t>(
|
||||
before->getFieldU64(sfOutstandingAmount));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta =
|
||||
balanceDelta.delta =
|
||||
static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltRIPPLE_STATE:
|
||||
balanceDelta = before->getFieldAmount(sfBalance);
|
||||
balanceDelta.delta = before->getFieldAmount(sfBalance);
|
||||
// Account balance is XRP, which is an int, so the scale is
|
||||
// always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltRIPPLE_STATE: {
|
||||
auto const amount = before->getFieldAmount(sfBalance);
|
||||
balanceDelta.delta = amount;
|
||||
// Trust Line balances are STAmounts, so we can use the exponent
|
||||
// directly to get the scale.
|
||||
balanceDelta.scale = amount.exponent();
|
||||
sign = -1;
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
}
|
||||
@@ -2707,20 +2723,36 @@ ValidVault::visitEntry(
|
||||
// At this moment we have no way of telling if this object holds
|
||||
// vault shares or something else. Save it for finalize.
|
||||
afterMPTs_.push_back(Shares::make(*after));
|
||||
balanceDelta -= Number(static_cast<std::int64_t>(
|
||||
balanceDelta.delta -= Number(static_cast<std::int64_t>(
|
||||
after->getFieldU64(sfOutstandingAmount)));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta -= Number(
|
||||
balanceDelta.delta -= Number(
|
||||
static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltRIPPLE_STATE:
|
||||
balanceDelta -= Number(after->getFieldAmount(sfBalance));
|
||||
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
|
||||
// Account balance is XRP, which is an int, so the scale is
|
||||
// always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltRIPPLE_STATE: {
|
||||
auto const amount = after->getFieldAmount(sfBalance);
|
||||
balanceDelta.delta -= Number(amount);
|
||||
// Trust Line balances are STAmounts, so we can use the exponent
|
||||
// directly to get the scale.
|
||||
balanceDelta.scale =
|
||||
std::max(balanceDelta.scale, amount.exponent());
|
||||
sign = -1;
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
}
|
||||
@@ -2732,7 +2764,10 @@ ValidVault::visitEntry(
|
||||
// transferred to the account. We intentionally do not compare balanceDelta
|
||||
// against zero, to avoid missing such updates.
|
||||
if (sign != 0)
|
||||
deltas_[key] = balanceDelta * sign;
|
||||
{
|
||||
balanceDelta.delta *= sign;
|
||||
deltas_[key] = balanceDelta;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -3012,13 +3047,15 @@ ValidVault::finalize(
|
||||
}
|
||||
|
||||
auto const& vaultAsset = afterVault.asset;
|
||||
auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
|
||||
auto const deltaAssets =
|
||||
[&](AccountID const& id) -> std::optional<DeltaInfo> {
|
||||
auto const get = //
|
||||
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
|
||||
[&](auto const& it,
|
||||
std::int8_t sign = 1) -> std::optional<DeltaInfo> {
|
||||
if (it == deltas_.end())
|
||||
return std::nullopt;
|
||||
|
||||
return it->second * sign;
|
||||
return DeltaInfo{it->second.delta * sign, it->second.scale};
|
||||
};
|
||||
|
||||
return std::visit(
|
||||
@@ -3039,7 +3076,7 @@ ValidVault::finalize(
|
||||
},
|
||||
vaultAsset.value());
|
||||
};
|
||||
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
|
||||
auto const deltaAssetsTxAccount = [&]() -> std::optional<DeltaInfo> {
|
||||
auto ret = deltaAssets(tx[sfAccount]);
|
||||
// Nothing returned or not XRP transaction
|
||||
if (!ret.has_value() || !vaultAsset.native())
|
||||
@@ -3050,13 +3087,14 @@ ValidVault::finalize(
|
||||
delegate.has_value() && *delegate != tx[sfAccount])
|
||||
return ret;
|
||||
|
||||
*ret += fee.drops();
|
||||
if (*ret == zero)
|
||||
ret->delta += fee.drops();
|
||||
if (ret->delta == zero)
|
||||
return std::nullopt;
|
||||
|
||||
return ret;
|
||||
};
|
||||
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
|
||||
auto const deltaShares =
|
||||
[&](AccountID const& id) -> std::optional<DeltaInfo> {
|
||||
auto const it = [&]() {
|
||||
if (id == afterVault.pseudoId)
|
||||
return deltas_.find(
|
||||
@@ -3064,7 +3102,7 @@ ValidVault::finalize(
|
||||
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
|
||||
}();
|
||||
|
||||
return it != deltas_.end() ? std::optional<Number>(it->second)
|
||||
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second)
|
||||
: std::nullopt;
|
||||
};
|
||||
|
||||
@@ -3196,16 +3234,41 @@ ValidVault::finalize(
|
||||
"xrpl::ValidVault::finalize : deposit updated a vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
auto const maybeVaultDeltaAssets =
|
||||
deltaAssets(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets > tx[sfAmount])
|
||||
// Get the coarsest scale to round calculations to
|
||||
DeltaInfo totalDelta{
|
||||
afterVault.assetsTotal - beforeVault.assetsTotal,
|
||||
std::max(
|
||||
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
|
||||
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
|
||||
DeltaInfo availableDelta{
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
std::max(
|
||||
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
|
||||
beforeVault.assetsAvailable.scale<STAmount>(
|
||||
vaultAsset))};
|
||||
auto const minScale = computeMinScale(
|
||||
vaultAsset,
|
||||
{
|
||||
*maybeVaultDeltaAssets,
|
||||
totalDelta,
|
||||
availableDelta,
|
||||
});
|
||||
|
||||
auto const vaultDeltaAssets = roundToAsset(
|
||||
vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
auto const txAmount =
|
||||
roundToAsset(vaultAsset, tx[sfAmount], minScale);
|
||||
|
||||
if (vaultDeltaAssets > txAmount)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must not change vault "
|
||||
@@ -3213,7 +3276,7 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets <= zero)
|
||||
if (vaultDeltaAssets <= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase vault balance";
|
||||
@@ -3230,16 +3293,24 @@ ValidVault::finalize(
|
||||
|
||||
if (!issuerDeposit)
|
||||
{
|
||||
auto const accountDeltaAssets = deltaAssetsTxAccount();
|
||||
if (!accountDeltaAssets)
|
||||
auto const maybeAccDeltaAssets = deltaAssetsTxAccount();
|
||||
if (!maybeAccDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor "
|
||||
"balance";
|
||||
return false;
|
||||
}
|
||||
auto const localMinScale = std::max(
|
||||
minScale,
|
||||
computeMinScale(vaultAsset, {*maybeAccDeltaAssets}));
|
||||
|
||||
if (*accountDeltaAssets >= zero)
|
||||
auto const accountDeltaAssets = roundToAsset(
|
||||
vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
|
||||
auto const localVaultDeltaAssets = roundToAsset(
|
||||
vaultAsset, vaultDeltaAssets, localMinScale);
|
||||
|
||||
if (accountDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must decrease depositor "
|
||||
@@ -3247,7 +3318,7 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
|
||||
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault and "
|
||||
@@ -3265,16 +3336,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!accountDeltaShares)
|
||||
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!maybeAccDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor "
|
||||
"shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*accountDeltaShares <= zero)
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const& accountDeltaShares = *maybeAccDeltaShares;
|
||||
if (accountDeltaShares.delta <= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase depositor "
|
||||
@@ -3282,15 +3354,19 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
auto const maybeVaultDeltaShares =
|
||||
deltaShares(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaShares ||
|
||||
maybeVaultDeltaShares->delta == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
|
||||
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor and "
|
||||
@@ -3298,15 +3374,22 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
|
||||
afterVault.assetsTotal)
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsTotal - beforeVault.assetsTotal,
|
||||
minScale);
|
||||
if (assetTotalDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||
"outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||
afterVault.assetsAvailable)
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||
"available must add up";
|
||||
@@ -3324,22 +3407,41 @@ ValidVault::finalize(
|
||||
"vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
auto const maybeVaultDeltaAssets =
|
||||
deltaAssets(afterVault.pseudoId);
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
if (!maybeVaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
|
||||
"change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
// Get the most coarse scale to round calculations to
|
||||
auto const totalDelta = DeltaInfo{
|
||||
afterVault.assetsTotal - beforeVault.assetsTotal,
|
||||
std::max(
|
||||
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
|
||||
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
|
||||
auto const availableDelta = DeltaInfo{
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
std::max(
|
||||
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
|
||||
beforeVault.assetsAvailable.scale<STAmount>(
|
||||
vaultAsset))};
|
||||
auto const minScale = computeMinScale(
|
||||
vaultAsset,
|
||||
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
|
||||
|
||||
auto const vaultPseudoDeltaAssets = roundToAsset(
|
||||
vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
|
||||
if (vaultPseudoDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
|
||||
"decrease vault balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Any payments (including withdrawal) going to the issuer
|
||||
// do not change their balance, but destroy funds instead.
|
||||
bool const issuerWithdrawal = [&]() -> bool {
|
||||
@@ -3352,17 +3454,17 @@ ValidVault::finalize(
|
||||
|
||||
if (!issuerWithdrawal)
|
||||
{
|
||||
auto const accountDeltaAssets = deltaAssetsTxAccount();
|
||||
auto const otherAccountDelta =
|
||||
[&]() -> std::optional<Number> {
|
||||
auto const maybeAccDelta = deltaAssetsTxAccount();
|
||||
auto const maybeOtherAccDelta =
|
||||
[&]() -> std::optional<DeltaInfo> {
|
||||
if (auto const destination = tx[~sfDestination];
|
||||
destination && *destination != tx[sfAccount])
|
||||
return deltaAssets(*destination);
|
||||
return std::nullopt;
|
||||
}();
|
||||
|
||||
if (accountDeltaAssets.has_value() ==
|
||||
otherAccountDelta.has_value())
|
||||
if (maybeAccDelta.has_value() ==
|
||||
maybeOtherAccDelta.has_value())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change one "
|
||||
@@ -3371,10 +3473,18 @@ ValidVault::finalize(
|
||||
}
|
||||
|
||||
auto const destinationDelta = //
|
||||
accountDeltaAssets ? *accountDeltaAssets
|
||||
: *otherAccountDelta;
|
||||
maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
|
||||
|
||||
if (destinationDelta <= zero)
|
||||
// the scale of destinationDelta can be coarser than
|
||||
// minScale, so we take that into account when rounding
|
||||
auto const localMinScale = std::max(
|
||||
minScale,
|
||||
computeMinScale(vaultAsset, {destinationDelta}));
|
||||
|
||||
auto const roundedDestinationDelta = roundToAsset(
|
||||
vaultAsset, destinationDelta.delta, localMinScale);
|
||||
|
||||
if (roundedDestinationDelta <= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must increase "
|
||||
@@ -3382,7 +3492,9 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets * -1 != destinationDelta)
|
||||
auto const localPseudoDeltaAssets = roundToAsset(
|
||||
vaultAsset, vaultPseudoDeltaAssets, localMinScale);
|
||||
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change vault "
|
||||
@@ -3390,7 +3502,7 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!accountDeltaShares)
|
||||
{
|
||||
@@ -3400,23 +3512,23 @@ ValidVault::finalize(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*accountDeltaShares >= zero)
|
||||
if (accountDeltaShares->delta >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must decrease depositor "
|
||||
"shares";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change depositor "
|
||||
@@ -3424,17 +3536,24 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsTotal - beforeVault.assetsTotal,
|
||||
minScale);
|
||||
// Note, vaultBalance is negative (see check above)
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
|
||||
afterVault.assetsTotal)
|
||||
if (assetTotalDelta != vaultPseudoDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||
"assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||
afterVault.assetsAvailable)
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
|
||||
if (assetAvailableDelta != vaultPseudoDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||
"assets available must add up";
|
||||
@@ -3468,10 +3587,30 @@ ValidVault::finalize(
|
||||
}
|
||||
}
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (vaultDeltaAssets)
|
||||
auto const maybeVaultDeltaAssets =
|
||||
deltaAssets(afterVault.pseudoId);
|
||||
if (maybeVaultDeltaAssets)
|
||||
{
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
auto const totalDelta = DeltaInfo{
|
||||
afterVault.assetsTotal - beforeVault.assetsTotal,
|
||||
std::max(
|
||||
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
|
||||
beforeVault.assetsTotal.scale<STAmount>(
|
||||
vaultAsset))};
|
||||
auto const availableDelta = DeltaInfo{
|
||||
afterVault.assetsAvailable -
|
||||
beforeVault.assetsAvailable,
|
||||
std::max(
|
||||
afterVault.assetsAvailable.scale<STAmount>(
|
||||
vaultAsset),
|
||||
beforeVault.assetsAvailable.scale<STAmount>(
|
||||
vaultAsset))};
|
||||
auto const minScale = computeMinScale(
|
||||
vaultAsset,
|
||||
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
|
||||
auto const vaultDeltaAssets = roundToAsset(
|
||||
vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
if (vaultDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease vault "
|
||||
@@ -3479,8 +3618,11 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
|
||||
afterVault.assetsTotal)
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsTotal - beforeVault.assetsTotal,
|
||||
minScale);
|
||||
if (assetsTotalDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding "
|
||||
@@ -3488,8 +3630,12 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||
afterVault.assetsAvailable)
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable -
|
||||
beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available "
|
||||
@@ -3504,15 +3650,15 @@ ValidVault::finalize(
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
|
||||
if (!accountDeltaShares)
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
|
||||
if (!maybeAccountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change holder shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*accountDeltaShares >= zero)
|
||||
if (maybeAccountDeltaShares->delta >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease holder "
|
||||
@@ -3520,15 +3666,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
if (vaultDeltaShares->delta * -1 !=
|
||||
maybeAccountDeltaShares->delta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change holder and "
|
||||
@@ -3566,4 +3714,22 @@ ValidVault::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::int32_t
|
||||
ValidVault::computeMinScale(
|
||||
Asset const& asset,
|
||||
std::initializer_list<DeltaInfo const> numbers)
|
||||
{
|
||||
if (numbers.size() == 0)
|
||||
return 0;
|
||||
|
||||
std::vector<std::int32_t> natScales;
|
||||
std::transform(
|
||||
numbers.begin(),
|
||||
numbers.end(),
|
||||
std::back_inserter(natScales),
|
||||
[&](DeltaInfo const& n) { return n.scale; });
|
||||
|
||||
return *std::max_element(natScales.begin(), natScales.end());
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -880,11 +879,19 @@ class ValidVault
|
||||
Shares static make(SLE const&);
|
||||
};
|
||||
|
||||
public:
|
||||
struct DeltaInfo final
|
||||
{
|
||||
Number delta = numZero;
|
||||
int scale = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
std::vector<Vault> afterVault_ = {};
|
||||
std::vector<Shares> afterMPTs_ = {};
|
||||
std::vector<Vault> beforeVault_ = {};
|
||||
std::vector<Shares> beforeMPTs_ = {};
|
||||
std::unordered_map<uint256, Number> deltas_ = {};
|
||||
std::unordered_map<uint256, DeltaInfo> deltas_ = {};
|
||||
|
||||
public:
|
||||
void
|
||||
@@ -900,6 +907,12 @@ public:
|
||||
XRPAmount const,
|
||||
ReadView const&,
|
||||
beast::Journal const&);
|
||||
|
||||
// Compute the coarsest scale required to represent all numbers
|
||||
[[nodiscard]] static std::int32_t
|
||||
computeMinScale(
|
||||
Asset const& asset,
|
||||
std::initializer_list<DeltaInfo const> numbers);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
|
||||
@@ -125,7 +125,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
tenthBipsOfValue(
|
||||
currentDebtTotal,
|
||||
TenthBips32(sleBroker->at(sfCoverRateMinimum))),
|
||||
currentDebtTotal.exponent());
|
||||
currentDebtTotal.scale<STAmount>(vaultAsset));
|
||||
}();
|
||||
if (coverAvail < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
@@ -1351,8 +1351,8 @@ PeerImp::handleTransaction(
|
||||
{
|
||||
// If we've never been in synch, there's nothing we can do
|
||||
// with a transaction
|
||||
JLOG(p_journal_.debug()) << "Ignoring incoming transaction: "
|
||||
<< "Need network ledger";
|
||||
JLOG(p_journal_.debug())
|
||||
<< "Ignoring incoming transaction: Need network ledger";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2618,6 +2618,16 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
newObj.set_ledgerseq(obj.ledgerseq());
|
||||
|
||||
// VFALCO NOTE "seq" in the message is obsolete
|
||||
|
||||
// Check if by adding this object, reply has reached its
|
||||
// limit
|
||||
if (reply.objects_size() >= Tuning::hardMaxReplyNodes)
|
||||
{
|
||||
fee_.update(
|
||||
Resource::feeModerateBurdenPeer,
|
||||
" Reply limit reached. Truncating reply.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,32 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
using FunctionType = std::function<Expected<uint256, Json::Value>(
|
||||
Json::Value const&,
|
||||
Json::StaticString const,
|
||||
unsigned const apiVersion)>;
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseFixed(
|
||||
Keylet const& keylet,
|
||||
Json::Value const& params,
|
||||
Json::StaticString const& fieldName,
|
||||
unsigned const apiVersion);
|
||||
|
||||
// Helper function to return FunctionType for objects that have a fixed
|
||||
// location. That is, they don't take parameters to compute the index.
|
||||
// e.g. amendments, fees, negative UNL, etc.
|
||||
static FunctionType
|
||||
fixed(Keylet const& keylet)
|
||||
{
|
||||
return [keylet](
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned const apiVersion) -> Expected<uint256, Json::Value> {
|
||||
return parseFixed(keylet, params, fieldName, apiVersion);
|
||||
};
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseObjectID(
|
||||
Json::Value const& params,
|
||||
@@ -33,13 +59,33 @@ parseObjectID(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseIndex(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseIndex(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned const apiVersion)
|
||||
{
|
||||
if (apiVersion > 2u && params.isString())
|
||||
{
|
||||
std::string const index = params.asString();
|
||||
if (index == jss::amendments.c_str())
|
||||
return keylet::amendments().key;
|
||||
if (index == jss::fee.c_str())
|
||||
return keylet::fees().key;
|
||||
if (index == jss::nunl)
|
||||
return keylet::negativeUNL().key;
|
||||
if (index == jss::hashes)
|
||||
// Note this only finds the "short" skip list. Use "hashes":index to
|
||||
// get the long list.
|
||||
return keylet::skip().key;
|
||||
}
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseAccountRoot(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (auto const account = LedgerEntryHelpers::parse<AccountID>(params))
|
||||
{
|
||||
@@ -50,14 +96,13 @@ parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName)
|
||||
"malformedAddress", fieldName, "AccountID");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseAmendments(Json::Value const& params, Json::StaticString const fieldName)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
auto const parseAmendments = fixed(keylet::amendments());
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseAMM(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseAMM(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -85,7 +130,10 @@ parseAMM(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseBridge(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseBridge(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isMember(jss::bridge))
|
||||
{
|
||||
@@ -116,13 +164,19 @@ parseBridge(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseCheck(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseCheck(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseCredential(Json::Value const& cred, Json::StaticString const fieldName)
|
||||
parseCredential(
|
||||
Json::Value const& cred,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!cred.isObject())
|
||||
{
|
||||
@@ -153,7 +207,10 @@ parseCredential(Json::Value const& cred, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDelegate(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseDelegate(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -244,7 +301,10 @@ parseAuthorizeCredentials(Json::Value const& jv)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName)
|
||||
parseDepositPreauth(
|
||||
Json::Value const& dp,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!dp.isObject())
|
||||
{
|
||||
@@ -297,7 +357,10 @@ parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDID(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseDID(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
auto const account = LedgerEntryHelpers::parse<AccountID>(params);
|
||||
if (!account)
|
||||
@@ -312,7 +375,8 @@ parseDID(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDirectoryNode(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -365,7 +429,10 @@ parseDirectoryNode(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseEscrow(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseEscrow(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -384,20 +451,53 @@ parseEscrow(Json::Value const& params, Json::StaticString const fieldName)
|
||||
return keylet::escrow(*id, *seq).key;
|
||||
}
|
||||
|
||||
auto const parseFeeSettings = fixed(keylet::fees());
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseFeeSettings(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseFixed(
|
||||
Keylet const& keylet,
|
||||
Json::Value const& params,
|
||||
Json::StaticString const& fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
if (!params.isBool())
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
if (!params.asBool())
|
||||
{
|
||||
return LedgerEntryHelpers::invalidFieldError(
|
||||
"invalidParams", fieldName, "true");
|
||||
}
|
||||
|
||||
return keylet.key;
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseLedgerHashes(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
if (params.isUInt() || params.isInt())
|
||||
{
|
||||
// If the index doesn't parse as a UInt, throw
|
||||
auto const index = params.asUInt();
|
||||
|
||||
// Return the "long" skip list for the given ledger index.
|
||||
auto const keylet = keylet::skip(index);
|
||||
return keylet.key;
|
||||
}
|
||||
// Return the key in `params` or the "short" skip list, which contains
|
||||
// hashes since the last flag ledger.
|
||||
return parseFixed(keylet::skip(), params, fieldName, apiVersion);
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseLoanBroker(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -417,7 +517,10 @@ parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseLoan(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseLoan(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -437,7 +540,10 @@ parseLoan(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseMPToken(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -460,7 +566,8 @@ parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parseMPTokenIssuance(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
auto const mptIssuanceID = LedgerEntryHelpers::parse<uint192>(params);
|
||||
if (!mptIssuanceID)
|
||||
@@ -471,25 +578,30 @@ parseMPTokenIssuance(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseNFTokenOffer(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseNFTokenOffer(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseNFTokenPage(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseNFTokenPage(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseNegativeUNL(Json::Value const& params, Json::StaticString const fieldName)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
auto const parseNegativeUNL = fixed(keylet::negativeUNL());
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseOffer(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseOffer(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -510,7 +622,10 @@ parseOffer(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseOracle(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseOracle(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -531,7 +646,10 @@ parseOracle(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parsePayChannel(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parsePayChannel(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
@@ -539,7 +657,8 @@ parsePayChannel(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parsePermissionedDomain(
|
||||
Json::Value const& pd,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (pd.isString())
|
||||
{
|
||||
@@ -568,7 +687,8 @@ parsePermissionedDomain(
|
||||
static Expected<uint256, Json::Value>
|
||||
parseRippleState(
|
||||
Json::Value const& jvRippleState,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
Currency uCurrency;
|
||||
|
||||
@@ -618,13 +738,19 @@ parseRippleState(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseSignerList(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseSignerList(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseTicket(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseTicket(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -645,7 +771,10 @@ parseTicket(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseVault(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseVault(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -668,7 +797,8 @@ parseVault(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parseXChainOwnedClaimID(
|
||||
Json::Value const& claim_id,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!claim_id.isObject())
|
||||
{
|
||||
@@ -693,7 +823,8 @@ parseXChainOwnedClaimID(
|
||||
static Expected<uint256, Json::Value>
|
||||
parseXChainOwnedCreateAccountClaimID(
|
||||
Json::Value const& claim_id,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
if (!claim_id.isObject())
|
||||
{
|
||||
@@ -717,10 +848,6 @@ parseXChainOwnedCreateAccountClaimID(
|
||||
return keylet.key;
|
||||
}
|
||||
|
||||
using FunctionType = Expected<uint256, Json::Value> (*)(
|
||||
Json::Value const&,
|
||||
Json::StaticString const);
|
||||
|
||||
struct LedgerEntry
|
||||
{
|
||||
Json::StaticString fieldName;
|
||||
@@ -753,7 +880,7 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
|
||||
});
|
||||
|
||||
auto hasMoreThanOneMember = [&]() {
|
||||
auto const hasMoreThanOneMember = [&]() {
|
||||
int count = 0;
|
||||
|
||||
for (auto const& ledgerEntry : ledgerEntryParsers)
|
||||
@@ -797,8 +924,8 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
Json::Value const& params = ledgerEntry.fieldName == jss::bridge
|
||||
? context.params
|
||||
: context.params[ledgerEntry.fieldName];
|
||||
auto const result =
|
||||
ledgerEntry.parseFunction(params, ledgerEntry.fieldName);
|
||||
auto const result = ledgerEntry.parseFunction(
|
||||
params, ledgerEntry.fieldName, context.apiVersion);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
@@ -829,9 +956,13 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
throw;
|
||||
}
|
||||
|
||||
// Return the computed index regardless of whether the node exists.
|
||||
jvResult[jss::index] = to_string(uNodeIndex);
|
||||
|
||||
if (uNodeIndex.isZero())
|
||||
{
|
||||
return RPC::make_error(rpcENTRY_NOT_FOUND);
|
||||
RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
auto const sleNode = lpLedger->read(keylet::unchecked(uNodeIndex));
|
||||
@@ -843,12 +974,14 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
if (!sleNode)
|
||||
{
|
||||
// Not found.
|
||||
return RPC::make_error(rpcENTRY_NOT_FOUND);
|
||||
RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
if ((expectedType != ltANY) && (expectedType != sleNode->getType()))
|
||||
{
|
||||
return RPC::make_error(rpcUNEXPECTED_LEDGER_TYPE);
|
||||
RPC::inject_error(rpcUNEXPECTED_LEDGER_TYPE, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
if (bNodeBinary)
|
||||
@@ -858,12 +991,10 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
sleNode->add(s);
|
||||
|
||||
jvResult[jss::node_binary] = strHex(s.peekData());
|
||||
jvResult[jss::index] = to_string(uNodeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
jvResult[jss::node] = sleNode->getJson(JsonOptions::none);
|
||||
jvResult[jss::index] = to_string(uNodeIndex);
|
||||
}
|
||||
|
||||
return jvResult;
|
||||
|
||||
Reference in New Issue
Block a user