From dfc95ec291051c6523ab98748bb974e058c55e99 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Tue, 28 Mar 2023 18:11:43 +0000 Subject: [PATCH] Sync CTID (#53) * ctid tests --- Builds/CMake/RippledCore.cmake | 1 + src/ripple/app/ledger/LedgerMaster.h | 4 +- src/ripple/app/ledger/impl/LedgerMaster.cpp | 5 +- src/ripple/net/impl/RPCCall.cpp | 6 +- src/ripple/rpc/CTID.h | 88 ++++ src/ripple/rpc/handlers/Tx.cpp | 102 ++--- src/ripple/rpc/impl/RPCHelpers.cpp | 3 + src/test/app/LedgerMaster_test.cpp | 129 ++++++ src/test/rpc/RPCCall_test.cpp | 44 +- src/test/rpc/Transaction_test.cpp | 430 +++++++++++++++++++- 10 files changed, 727 insertions(+), 85 deletions(-) create mode 100644 src/ripple/rpc/CTID.h create mode 100644 src/test/app/LedgerMaster_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index dadb2f4f7..85ff3dbc5 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -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 diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index 683c398b0..1b1dc15dd 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -291,8 +291,8 @@ public: // Returns the minimum ledger sequence in SQL database, if any. std::optional 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 txnIDfromIndex(uint32_t ledgerSeq, uint32_t txnIndex); diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index be2612955..009c79669 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -2373,7 +2373,6 @@ LedgerMaster::minSqlSeq() return app_.getRelationalDatabase().getMinLedgerSeq(); } - std::optional 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 {}; diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 1ec91a55a..42e9564b8 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -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; } diff --git a/src/ripple/rpc/CTID.h b/src/ripple/rpc/CTID.h new file mode 100644 index 000000000..4d981f40c --- /dev/null +++ b/src/ripple/rpc/CTID.h @@ -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 +#include +#include +#include + +namespace ripple { + +namespace RPC { + +inline std::optional +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(ledger_seq)) << 32) + + (static_cast(txn_index) << 16) + network_id; + + std::stringstream buffer; + buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16) + << ctidValue; + return {buffer.str()}; +} + +template +inline std::optional> +decodeCTID(const T ctid) noexcept +{ + uint64_t ctidValue{0}; + if constexpr ( + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v) + { + 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) + 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 diff --git a/src/ripple/rpc/handlers/Tx.cpp b/src/ripple/rpc/handlers/Tx.cpp index 13b5794d9..e9c4439f9 100644 --- a/src/ripple/rpc/handlers/Tx.cpp +++ b/src/ripple/rpc/handlers/Tx.cpp @@ -28,66 +28,13 @@ #include #include #include +#include #include #include #include namespace ripple { -// { -// transaction: | -// ctid: -// } - -std::optional -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(ledger_seq)) << 32) + - (static_cast(txn_index) << 16) + network_id; - - std::stringstream buffer; - buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << ctidValue; - return {buffer.str()}; -} - -template -std::optional> -decodeCTID(const T ctid) noexcept -{ - uint64_t ctidValue {0}; - if constexpr (std::is_same_v || std::is_same_v || - std::is_same_v || - std::is_same_v) - { - 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) - 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( "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> 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(args.ctid->first, args.ctid->second); + args.hash = context.app.getLedgerMaster().txnIDfromIndex( + args.ctid->first, args.ctid->second); + + if (args.hash) + range = + ClosedInterval(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}; } diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 5cf2270b3..9e4637b38 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -35,6 +35,9 @@ #include #include +#include +#include + namespace ripple { namespace RPC { diff --git a/src/test/app/LedgerMaster_test.cpp b/src/test/app/LedgerMaster_test.cpp new file mode 100644 index 000000000..3f7165889 --- /dev/null +++ b/src/test/app/LedgerMaster_test.cpp @@ -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 +#include +#include +#include + +namespace ripple { +namespace test { + +class LedgerMaster_test : public beast::unit_test::suite +{ + std::unique_ptr + makeNetworkConfig(uint32_t networkID) + { + using namespace jtx; + return envconfig([&](std::unique_ptr 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> txns; + std::vector> 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 diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index 3f55c982c..0d4702799 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -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 +makeNetworkConfig(uint32_t networkID) +{ + using namespace test::jtx; + return envconfig([&](std::unique_ptr 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) diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 08e97c1c2..8732d083f 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -20,16 +20,29 @@ #include #include #include +#include +#include #include #include #include +#include namespace ripple { class Transaction_test : public beast::unit_test::suite { + std::unique_ptr + makeNetworkConfig(uint32_t networkID) + { + using namespace test::jtx; + return envconfig([&](std::unique_ptr 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> txns; + std::vector> 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(&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("CFFFFFFFFFFFFFFF"); + BEAST_EXPECT( + RPC::encodeCTID(0xFFFFFFFUL, 0xFFFFU, 0xFFFFU) == expected11); + auto const expected12 = std::optional("C000000000000000"); + BEAST_EXPECT(RPC::encodeCTID(0, 0, 0) == expected12); + auto const expected13 = std::optional("C000000100020003"); + BEAST_EXPECT(RPC::encodeCTID(1U, 2U, 3U) == expected13); + auto const expected14 = std::optional("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("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("CFFFFFFFFFFF0000"); + BEAST_EXPECT( + RPC::encodeCTID(0xFFFFFFFUL, 0xFFFFU, (uint16_t)0x10000U) == + expected4); + + // Test case 5: Valid input values + auto const expected51 = + std::optional>( + std::make_tuple(0, 0, 0)); + BEAST_EXPECT(RPC::decodeCTID("C000000000000000") == expected51); + auto const expected52 = + std::optional>( + std::make_tuple(1U, 2U, 3U)); + BEAST_EXPECT(RPC::decodeCTID("C000000100020003") == expected52); + auto const expected53 = + std::optional>( + 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::make_tuple(0xFFFFFFFUL, 0xFFFFU, 0xFFFFU)))); + BEAST_EXPECT( + (RPC::decodeCTID(0xC000000000000000ULL) == + std::optional>( + std::make_tuple(0, 0, 0)))); + BEAST_EXPECT( + (RPC::decodeCTID(0xC000000100020003ULL) == + std::optional>( + std::make_tuple(1U, 2U, 3U)))); + BEAST_EXPECT( + (RPC::decodeCTID(0xC0CA2AA7326FC045ULL) == + std::optional>( + 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); } };