Sync CTID (#53)

* ctid tests
This commit is contained in:
Denis Angell
2023-03-28 18:11:43 +00:00
parent 81d795dcfc
commit dfc95ec291
10 changed files with 727 additions and 85 deletions

View File

@@ -704,6 +704,7 @@ if (tests)
src/test/app/HashRouter_test.cpp
src/test/app/LedgerHistory_test.cpp
src/test/app/LedgerLoad_test.cpp
src/test/app/LedgerMaster_test.cpp
src/test/app/LedgerReplay_test.cpp
src/test/app/LoadFeeTrack_test.cpp
src/test/app/Manifest_test.cpp

View File

@@ -291,8 +291,8 @@ public:
// Returns the minimum ledger sequence in SQL database, if any.
std::optional<LedgerIndex>
minSqlSeq();
// Iff a txn exists at the specified ledger and offset then return its txnid
// Iff a txn exists at the specified ledger and offset then return its txnid
std::optional<uint256>
txnIDfromIndex(uint32_t ledgerSeq, uint32_t txnIndex);

View File

@@ -2373,7 +2373,6 @@ LedgerMaster::minSqlSeq()
return app_.getRelationalDatabase().getMinLedgerSeq();
}
std::optional<uint256>
LedgerMaster::txnIDfromIndex(uint32_t ledgerSeq, uint32_t txnIndex)
{
@@ -2388,8 +2387,8 @@ LedgerMaster::txnIDfromIndex(uint32_t ledgerSeq, uint32_t txnIndex)
for (auto it = lgr->txs.begin(); it != lgr->txs.end(); it++)
if (it->first && it->second &&
it->second->isFieldPresent(sfTransactionIndex) &&
it->second->getFieldU32(sfTransactionIndex) == txnIndex)
it->second->isFieldPresent(sfTransactionIndex) &&
it->second->getFieldU32(sfTransactionIndex) == txnIndex)
return it->first->getTransactionID();
return {};

View File

@@ -1267,7 +1267,11 @@ private:
jvRequest[jss::max_ledger] = jvParams[2u + offset].asString();
}
jvRequest[jss::transaction] = jvParams[0u].asString();
if (jvParams[0u].asString().length() == 16)
jvRequest[jss::ctid] = jvParams[0u].asString();
else
jvRequest[jss::transaction] = jvParams[0u].asString();
return jvRequest;
}

88
src/ripple/rpc/CTID.h Normal file
View File

@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_RPC_CTID_H_INCLUDED
#define RIPPLE_RPC_CTID_H_INCLUDED
#include <boost/algorithm/string/predicate.hpp>
#include <optional>
#include <regex>
#include <sstream>
namespace ripple {
namespace RPC {
inline std::optional<std::string>
encodeCTID(
uint32_t ledger_seq,
uint16_t txn_index,
uint16_t network_id) noexcept
{
if (ledger_seq > 0xFFFFFFF)
return {};
uint64_t ctidValue =
((0xC0000000ULL + static_cast<uint64_t>(ledger_seq)) << 32) +
(static_cast<uint64_t>(txn_index) << 16) + network_id;
std::stringstream buffer;
buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16)
<< ctidValue;
return {buffer.str()};
}
template <typename T>
inline std::optional<std::tuple<uint32_t, uint16_t, uint16_t>>
decodeCTID(const T ctid) noexcept
{
uint64_t ctidValue{0};
if constexpr (
std::is_same_v<T, std::string> || std::is_same_v<T, char*> ||
std::is_same_v<T, const char*> || std::is_same_v<T, std::string_view>)
{
const std::string ctidString(ctid);
if (ctidString.length() != 16)
return {};
if (!std::regex_match(ctidString, std::regex("^[0-9A-F]+$")))
return {};
ctidValue = std::stoull(ctidString, nullptr, 16);
}
else if constexpr (std::is_integral_v<T>)
ctidValue = ctid;
else
return {};
if (ctidValue > 0xFFFFFFFFFFFFFFFFULL ||
(ctidValue & 0xF000000000000000ULL) != 0xC000000000000000ULL)
return {};
uint32_t ledger_seq = (ctidValue >> 32) & 0xFFFFFFFUL;
uint16_t txn_index = (ctidValue >> 16) & 0xFFFFU;
uint16_t network_id = ctidValue & 0xFFFFU;
return {{ledger_seq, txn_index, network_id}};
}
} // namespace RPC
} // namespace ripple
#endif

View File

@@ -28,66 +28,13 @@
#include <ripple/rpc/Context.h>
#include <ripple/rpc/DeliveredAmount.h>
#include <ripple/rpc/GRPCHandlers.h>
#include <ripple/rpc/CTID.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <charconv>
#include <regex>
namespace ripple {
// {
// transaction: <hex> |
// ctid: <hex>
// }
std::optional<std::string>
encodeCTID(uint32_t ledger_seq, uint16_t txn_index, uint16_t network_id) noexcept
{
if (ledger_seq > 0xFFFFFFF)
return {};
uint64_t ctidValue =
((0xC0000000ULL + static_cast<uint64_t>(ledger_seq)) << 32) +
(static_cast<uint64_t>(txn_index) << 16) + network_id;
std::stringstream buffer;
buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << ctidValue;
return {buffer.str()};
}
template <typename T>
std::optional<std::tuple<uint32_t, uint16_t, uint16_t>>
decodeCTID(const T ctid) noexcept
{
uint64_t ctidValue {0};
if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, char *> ||
std::is_same_v<T, const char *> ||
std::is_same_v<T, std::string_view>)
{
const std::string ctidString(ctid);
if (ctidString.length() != 16)
return {};
if (!std::regex_match(ctidString, std::regex("^[0-9A-F]+$")))
return {};
ctidValue = std::stoull(ctidString, nullptr, 16);
}
else if constexpr (std::is_integral_v<T>)
ctidValue = ctid;
else
return {};
if (ctidValue > 0xFFFFFFFFFFFFFFFFULL ||
(ctidValue & 0xF000000000000000ULL) != 0xC000000000000000ULL)
return {};
uint32_t ledger_seq = (ctidValue >> 32) & 0xFFFFFFFUL;
uint16_t txn_index = (ctidValue >> 16) & 0xFFFFU;
uint16_t network_id = ctidValue & 0xFFFFU;
return {{ledger_seq, txn_index, network_id}};
}
static bool
isValidated(LedgerMaster& ledgerMaster, std::uint32_t seq, uint256 const& hash)
{
@@ -126,16 +73,19 @@ doTxPostgres(RPC::Context& context, TxArgs const& args)
Throw<std::runtime_error>(
"Called doTxPostgres yet not in reporting mode");
}
TxResult res;
res.searchedAll = TxSearched::unknown;
if (!args.hash)
return {res, {rpcNOT_IMPL, "Use of CTIDs on reporting mode is not currently supported."}};
if (!args.hash)
return {
res,
{rpcNOT_IMPL,
"Use of CTIDs on reporting mode is not currently supported."}};
JLOG(context.j.debug()) << "Fetching from postgres";
Transaction::Locator locator = Transaction::locate(*(args.hash), context.app);
Transaction::Locator locator =
Transaction::locate(*(args.hash), context.app);
std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
pair;
@@ -258,14 +208,14 @@ doTxHelp(RPC::Context& context, TxArgs args)
if (args.ctid)
{
args.hash =
context.app.getLedgerMaster().
txnIDfromIndex(args.ctid->first, args.ctid->second);
if (!args.hash)
return {result, rpcTXN_NOT_FOUND};
range = ClosedInterval<uint32_t>(args.ctid->first, args.ctid->second);
args.hash = context.app.getLedgerMaster().txnIDfromIndex(
args.ctid->first, args.ctid->second);
if (args.hash)
range =
ClosedInterval<uint32_t>(args.ctid->first, args.ctid->second);
}
if (args.ledgerRange)
{
v = context.app.getMasterTransaction().fetch(*(args.hash), range, ec);
@@ -321,8 +271,7 @@ doTxHelp(RPC::Context& context, TxArgs args)
uint32_t netID = context.app.config().NETWORK_ID;
if (txnIdx <= 0xFFFFU && netID < 0xFFFFU && lgrSeq < 0xFFFFFFFUL)
result.ctid =
encodeCTID(lgrSeq, (uint16_t)txnIdx, (uint16_t)netID);
result.ctid = RPC::encodeCTID(lgrSeq, (uint16_t)txnIdx, (uint16_t)netID);
}
return {result, rpcSUCCESS};
@@ -393,19 +342,21 @@ doTxJson(RPC::JsonContext& context)
TxArgs args;
if (context.params.isMember(jss::transaction) && context.params.isMember(jss::ctid))
if (context.params.isMember(jss::transaction) &&
context.params.isMember(jss::ctid))
// specifying both is ambiguous
return rpcError(rpcINVALID_PARAMS);
if (context.params.isMember(jss::transaction))
{
uint256 hash;
if (!hash.parseHex(context.params[jss::transaction].asString()))
return rpcError(rpcNOT_IMPL);
args.hash = hash;
} else if (context.params.isMember(jss::ctid)) {
auto ctid = decodeCTID(context.params[jss::ctid].asString());
}
else if (context.params.isMember(jss::ctid))
{
auto ctid = RPC::decodeCTID(context.params[jss::ctid].asString());
if (!ctid)
return rpcError(rpcINVALID_PARAMS);
@@ -413,9 +364,10 @@ doTxJson(RPC::JsonContext& context)
if (net_id != context.app.config().NETWORK_ID)
{
std::stringstream out;
out << "Wrong network. You should submit this request to a node running on NetworkID: " << net_id;
return RPC::make_error(
rpcWRONG_NETWORK, out.str());
out << "Wrong network. You should submit this request to a node "
"running on NetworkID: "
<< net_id;
return RPC::make_error(rpcWRONG_NETWORK, out.str());
}
args.ctid = {lgr_seq, txn_idx};
}

View File

@@ -35,6 +35,9 @@
#include <ripple/rpc/impl/RPCHelpers.h>
#include <boost/algorithm/string/case_conv.hpp>
#include <ripple/resource/Fees.h>
#include <regex>
namespace ripple {
namespace RPC {

View File

@@ -0,0 +1,129 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 XRPLF
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#include <test/jtx/Env.h>
namespace ripple {
namespace test {
class LedgerMaster_test : public beast::unit_test::suite
{
std::unique_ptr<Config>
makeNetworkConfig(uint32_t networkID)
{
using namespace jtx;
return envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = networkID;
return cfg;
});
}
void
testTxnIDfromIndex(FeatureBitset features)
{
testcase("tx_id_from_index");
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, makeNetworkConfig(11111)};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
// build ledgers
std::vector<std::shared_ptr<STTx const>> txns;
std::vector<std::shared_ptr<STObject const>> metas;
auto const startLegSeq = env.current()->info().seq;
for (int i = 0; i < 2; ++i)
{
env(noop(alice));
txns.emplace_back(env.tx());
env.close();
metas.emplace_back(
env.closed()->txRead(env.tx()->getTransactionID()).second);
}
// add last (empty) ledger
env.close();
auto const endLegSeq = env.closed()->info().seq;
// test invalid range
{
std::uint32_t ledgerSeq = -1;
std::uint32_t txnIndex = 0;
auto result =
env.app().getLedgerMaster().txnIDfromIndex(ledgerSeq, txnIndex);
BEAST_EXPECT(!result);
}
// test not in ledger
{
uint32_t txnIndex = metas[0]->getFieldU32(sfTransactionIndex);
auto result =
env.app().getLedgerMaster().txnIDfromIndex(0, txnIndex);
BEAST_EXPECT(!result);
}
// test empty ledger
{
auto result =
env.app().getLedgerMaster().txnIDfromIndex(endLegSeq, 0);
BEAST_EXPECT(!result);
}
// ended without result
{
uint32_t txnIndex = metas[0]->getFieldU32(sfTransactionIndex);
auto result = env.app().getLedgerMaster().txnIDfromIndex(
endLegSeq + 1, txnIndex);
BEAST_EXPECT(!result);
}
// success
{
uint32_t txnIndex = metas[0]->getFieldU32(sfTransactionIndex);
auto result = env.app().getLedgerMaster().txnIDfromIndex(
startLegSeq, txnIndex);
BEAST_EXPECT(
*result ==
uint256("277F4FD89C20B92457FEF05FF63F6405563AD0563C73D967A29727"
"72679ADC65"));
}
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
testWithFeats(all);
}
void
testWithFeats(FeatureBitset features)
{
testTxnIDfromIndex(features);
}
};
BEAST_DEFINE_TESTSUITE(LedgerMaster, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -5746,6 +5746,37 @@ static RPCCallTestData const rpcCallTestArray[] = {
// tx
// --------------------------------------------------------------------------
{"tx: ctid. minimal",
__LINE__,
{"tx", "FFFFFFFFFFFFFFFF", "1", "2"},
RPCCallTestData::no_exception,
R"({
"method" : "tx",
"params" : [
{
"api_version" : %MAX_API_VER%,
"ctid" : "FFFFFFFFFFFFFFFF",
"max_ledger" : "2",
"min_ledger" : "1"
}
]
})"},
{"tx: ctid. binary",
__LINE__,
{"tx", "FFFFFFFFFFFFFFFF", "binary", "1", "2"},
RPCCallTestData::no_exception,
R"({
"method" : "tx",
"params" : [
{
"api_version" : %MAX_API_VER%,
"binary" : true,
"ctid" : "FFFFFFFFFFFFFFFF",
"max_ledger" : "2",
"min_ledger" : "1"
}
]
})"},
{"tx: minimal.",
__LINE__,
{"tx", "transaction_hash_is_not_validated"},
@@ -6532,6 +6563,16 @@ updateAPIVersionString(const char* const req)
return jr;
}
std::unique_ptr<Config>
makeNetworkConfig(uint32_t networkID)
{
using namespace test::jtx;
return envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = networkID;
return cfg;
});
}
class RPCCall_test : public beast::unit_test::suite
{
public:
@@ -6540,7 +6581,8 @@ public:
{
testcase << "RPCCall";
test::jtx::Env env(*this); // Used only for its Journal.
test::jtx::Env env(
*this, makeNetworkConfig(11111)); // Used only for its Journal.
// For each RPCCall test.
for (RPCCallTestData const& rpcCallTest : rpcCallTestArray)

View File

@@ -20,16 +20,29 @@
#include <ripple/app/rdb/backend/SQLiteDatabase.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/jss.h>
#include <ripple/rpc/CTID.h>
#include <optional>
#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <tuple>
namespace ripple {
class Transaction_test : public beast::unit_test::suite
{
std::unique_ptr<Config>
makeNetworkConfig(uint32_t networkID)
{
using namespace test::jtx;
return envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = networkID;
return cfg;
});
}
void
testRangeRequest()
testRangeRequest(FeatureBitset features)
{
testcase("Test Range Request");
@@ -43,7 +56,7 @@ class Transaction_test : public beast::unit_test::suite
const char* EXCESSIVE =
RPC::get_error_info(rpcEXCESSIVE_LGR_RANGE).token;
Env env(*this);
Env env{*this, features};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
@@ -278,11 +291,422 @@ class Transaction_test : public beast::unit_test::suite
}
}
void
testRangeCTIDRequest(FeatureBitset features)
{
testcase("ctid_range");
using namespace test::jtx;
using std::to_string;
const char* COMMAND = jss::tx.c_str();
const char* BINARY = jss::binary.c_str();
const char* NOT_FOUND = RPC::get_error_info(rpcTXN_NOT_FOUND).token;
const char* INVALID = RPC::get_error_info(rpcINVALID_LGR_RANGE).token;
const char* EXCESSIVE =
RPC::get_error_info(rpcEXCESSIVE_LGR_RANGE).token;
Env env{*this, makeNetworkConfig(11111)};
uint32_t netID = env.app().config().NETWORK_ID;
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
std::vector<std::shared_ptr<STTx const>> txns;
std::vector<std::shared_ptr<STObject const>> metas;
auto const startLegSeq = env.current()->info().seq;
for (int i = 0; i < 750; ++i)
{
env(noop(alice));
txns.emplace_back(env.tx());
env.close();
metas.emplace_back(
env.closed()->txRead(env.tx()->getTransactionID()).second);
}
auto const endLegSeq = env.closed()->info().seq;
// Find the existing transactions
for (size_t i = 0; i < txns.size(); ++i)
{
auto const& tx = txns[i];
auto const& meta = metas[i];
uint32_t txnIdx = meta->getFieldU32(sfTransactionIndex);
auto const result = env.rpc(
COMMAND,
*RPC::encodeCTID(startLegSeq + i, txnIdx, netID),
BINARY,
to_string(startLegSeq),
to_string(endLegSeq));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(
result[jss::result][jss::tx] ==
strHex(tx->getSerializer().getData()));
BEAST_EXPECT(
result[jss::result][jss::meta] ==
strHex(meta->getSerializer().getData()));
}
auto const tx = env.jt(noop(alice), seq(env.seq(alice))).stx;
auto const ctid =
*RPC::encodeCTID(endLegSeq, tx->getSeqProxy().value(), netID);
for (int deltaEndSeq = 0; deltaEndSeq < 3; ++deltaEndSeq)
{
auto const result = env.rpc(
COMMAND,
ctid,
BINARY,
to_string(startLegSeq),
to_string(endLegSeq + deltaEndSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
if (deltaEndSeq)
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
else
BEAST_EXPECT(result[jss::result][jss::searched_all].asBool());
}
// Find transactions outside of provided range.
for (size_t i = 0; i < txns.size(); ++i)
{
// auto const& tx = txns[i];
auto const& meta = metas[i];
uint32_t txnIdx = meta->getFieldU32(sfTransactionIndex);
auto const result = env.rpc(
COMMAND,
*RPC::encodeCTID(startLegSeq + i, txnIdx, netID),
BINARY,
to_string(endLegSeq + 1),
to_string(endLegSeq + 100));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
const auto deletedLedger = (startLegSeq + endLegSeq) / 2;
{
// Remove one of the ledgers from the database directly
dynamic_cast<SQLiteDatabase*>(&env.app().getRelationalDatabase())
->deleteTransactionByLedgerSeq(deletedLedger);
}
for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq)
{
auto const result = env.rpc(
COMMAND,
ctid,
BINARY,
to_string(startLegSeq),
to_string(endLegSeq + deltaEndSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result = env.rpc(
COMMAND, ctid, to_string(startLegSeq), to_string(endLegSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(!result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const result = env.rpc(
COMMAND,
ctid,
to_string(startLegSeq),
to_string(deletedLedger - 1));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == NOT_FOUND);
BEAST_EXPECT(result[jss::result][jss::searched_all].asBool());
}
// Provide range without providing the `binary`
// field. (Tests parameter parsing)
{
auto const& meta = metas[0];
uint32_t txnIdx = meta->getFieldU32(sfTransactionIndex);
auto const result = env.rpc(
COMMAND,
*RPC::encodeCTID(endLegSeq, txnIdx, netID),
to_string(startLegSeq),
to_string(deletedLedger - 1));
BEAST_EXPECT(result[jss::result][jss::status] == jss::success);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min > max)
{
auto const result = env.rpc(
COMMAND,
ctid,
BINARY,
to_string(deletedLedger - 1),
to_string(startLegSeq));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min < 0)
{
auto const result = env.rpc(
COMMAND,
ctid,
BINARY,
to_string(-1),
to_string(deletedLedger - 1));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (min < 0, max < 0)
{
auto const result =
env.rpc(COMMAND, ctid, BINARY, to_string(-20), to_string(-10));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (only one value)
{
auto const result = env.rpc(COMMAND, ctid, BINARY, to_string(20));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == INVALID);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (only one value)
{
auto const result = env.rpc(COMMAND, ctid, to_string(20));
// Since we only provided one value for the range,
// the interface parses it as a false binary flag,
// as single-value ranges are not accepted. Since
// the error this causes differs depending on the platform
// we don't call out a specific error here.
BEAST_EXPECT(result[jss::result][jss::status] == jss::error);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
// Provide an invalid range: (max - min > 1000)
{
auto const result = env.rpc(
COMMAND,
ctid,
BINARY,
to_string(startLegSeq),
to_string(startLegSeq + 1001));
BEAST_EXPECT(
result[jss::result][jss::status] == jss::error &&
result[jss::result][jss::error] == EXCESSIVE);
BEAST_EXPECT(!result[jss::result].isMember(jss::searched_all));
}
}
void
testCTIDValidation(FeatureBitset features)
{
testcase("ctid_validation");
using namespace test::jtx;
using std::to_string;
Env env{*this, makeNetworkConfig(11111)};
// Test case 1: Valid input values
auto const expected11 = std::optional<std::string>("CFFFFFFFFFFFFFFF");
BEAST_EXPECT(
RPC::encodeCTID(0xFFFFFFFUL, 0xFFFFU, 0xFFFFU) == expected11);
auto const expected12 = std::optional<std::string>("C000000000000000");
BEAST_EXPECT(RPC::encodeCTID(0, 0, 0) == expected12);
auto const expected13 = std::optional<std::string>("C000000100020003");
BEAST_EXPECT(RPC::encodeCTID(1U, 2U, 3U) == expected13);
auto const expected14 = std::optional<std::string>("C0CA2AA7326FFFFF");
BEAST_EXPECT(RPC::encodeCTID(13249191UL, 12911U, 65535U) == expected14);
// Test case 2: ledger_seq greater than 0xFFFFFFF
BEAST_EXPECT(!RPC::encodeCTID(0x10000000UL, 0xFFFFU, 0xFFFFU));
// Test case 3: txn_index greater than 0xFFFF
// this test case is impossible in c++ due to the type, left in for
// completeness
auto const expected3 = std::optional<std::string>("CFFFFFFF0000FFFF");
BEAST_EXPECT(
RPC::encodeCTID(0xFFFFFFF, (uint16_t)0x10000, 0xFFFF) == expected3);
// Test case 4: network_id greater than 0xFFFF
// this test case is impossible in c++ due to the type, left in for
// completeness
auto const expected4 = std::optional<std::string>("CFFFFFFFFFFF0000");
BEAST_EXPECT(
RPC::encodeCTID(0xFFFFFFFUL, 0xFFFFU, (uint16_t)0x10000U) ==
expected4);
// Test case 5: Valid input values
auto const expected51 =
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(0, 0, 0));
BEAST_EXPECT(RPC::decodeCTID("C000000000000000") == expected51);
auto const expected52 =
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(1U, 2U, 3U));
BEAST_EXPECT(RPC::decodeCTID("C000000100020003") == expected52);
auto const expected53 =
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(13249191UL, 12911U, 49221U));
BEAST_EXPECT(RPC::decodeCTID("C0CA2AA7326FC045") == expected53);
// Test case 6: ctid not a string or big int
BEAST_EXPECT(!RPC::decodeCTID(0xCFF));
// Test case 7: ctid not a hexadecimal string
BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFFG"));
// Test case 8: ctid not exactly 16 nibbles
BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFF"));
// Test case 9: ctid too large to be a valid CTID value
BEAST_EXPECT(!RPC::decodeCTID("CFFFFFFFFFFFFFFFF"));
// Test case 10: ctid doesn't start with a C nibble
BEAST_EXPECT(!RPC::decodeCTID("FFFFFFFFFFFFFFFF"));
// Test case 11: Valid input values
BEAST_EXPECT(
(RPC::decodeCTID(0xCFFFFFFFFFFFFFFFULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(0xFFFFFFFUL, 0xFFFFU, 0xFFFFU))));
BEAST_EXPECT(
(RPC::decodeCTID(0xC000000000000000ULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(0, 0, 0))));
BEAST_EXPECT(
(RPC::decodeCTID(0xC000000100020003ULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(1U, 2U, 3U))));
BEAST_EXPECT(
(RPC::decodeCTID(0xC0CA2AA7326FC045ULL) ==
std::optional<std::tuple<int32_t, uint16_t, uint16_t>>(
std::make_tuple(13249191UL, 12911U, 49221U))));
// Test case 12: ctid not exactly 16 nibbles
BEAST_EXPECT(!RPC::decodeCTID(0xC003FFFFFFFFFFF));
// Test case 13: ctid too large to be a valid CTID value
// this test case is not possible in c++ because it would overflow the
// type, left in for completeness
// BEAST_EXPECT(!RPC::decodeCTID(0xCFFFFFFFFFFFFFFFFULL));
// Test case 14: ctid doesn't start with a C nibble
BEAST_EXPECT(!RPC::decodeCTID(0xFFFFFFFFFFFFFFFFULL));
}
void
testCTIDRPC(FeatureBitset features)
{
testcase("ctid_rpc");
using namespace test::jtx;
// test that the ctid AND the hash are in the response
{
Env env{*this, makeNetworkConfig(11111)};
uint32_t netID = env.app().config().NETWORK_ID;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const startLegSeq = env.current()->info().seq;
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();
auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID);
Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = ctid;
jsonTx[jss::id] = 1;
auto jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(jrr[jss::ctid] == ctid);
BEAST_EXPECT(jrr[jss::hash]);
}
// test that if the network is 65535 the ctid is not in the response
{
Env env{*this, makeNetworkConfig(65535)};
uint32_t netID = env.app().config().NETWORK_ID;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const startLegSeq = env.current()->info().seq;
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(10)));
env.close();
auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID);
Json::Value jsonTx;
jsonTx[jss::binary] = false;
jsonTx[jss::ctid] = ctid;
jsonTx[jss::id] = 1;
auto jrr = env.rpc("json", "tx", to_string(jsonTx))[jss::result];
BEAST_EXPECT(!jrr[jss::ctid]);
BEAST_EXPECT(jrr[jss::hash]);
}
}
public:
void
run() override
{
testRangeRequest();
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
testWithFeats(all);
}
void
testWithFeats(FeatureBitset features)
{
testRangeRequest(features);
testRangeCTIDRequest(features);
testCTIDValidation(features);
testCTIDRPC(features);
}
};