From 798604f6ddcb233d0046448919a50b2b17304d50 Mon Sep 17 00:00:00 2001 From: RichardAH Date: Fri, 18 Aug 2023 03:43:47 +0200 Subject: [PATCH] feat: support Concise Transaction Identifier (CTID) (XLS-37) (#4418) * add CTIM to tx rpc --------- Co-authored-by: Rome Reginelli Co-authored-by: Elliot Lee Co-authored-by: Denis Angell --- Builds/CMake/RippledCore.cmake | 1 + src/ripple/app/ledger/LedgerMaster.h | 4 + src/ripple/app/ledger/impl/LedgerMaster.cpp | 21 + src/ripple/net/impl/RPCCall.cpp | 6 +- src/ripple/protocol/ErrorCodes.h | 2 +- src/ripple/protocol/jss.h | 1 + src/ripple/rpc/CTID.h | 88 ++++ src/ripple/rpc/handlers/Tx.cpp | 90 +++- src/ripple/rpc/impl/RPCHelpers.cpp | 1 + src/test/app/LedgerMaster_test.cpp | 139 +++++++ src/test/app/NetworkID_test.cpp | 26 +- src/test/jtx/Env.h | 12 + src/test/jtx/impl/Env.cpp | 5 + src/test/rpc/RPCCall_test.cpp | 44 +- src/test/rpc/Transaction_test.cpp | 431 +++++++++++++++++++- 15 files changed, 843 insertions(+), 28 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 b676c5ff5e9..3240f79f896 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -768,6 +768,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 802df8eb5cb..3d7adc86223 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -292,6 +292,10 @@ class LedgerMaster : public AbstractFetchPackContainer std::optional minSqlSeq(); + // Iff a txn exists at the specified ledger and offset then return its txnid + std::optional + txnIdFromIndex(uint32_t ledgerSeq, uint32_t txnIndex); + private: void setValidLedger(std::shared_ptr const& l); diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 7ae7476948b..ff42a88a84f 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -2369,4 +2369,25 @@ LedgerMaster::minSqlSeq() return app_.getRelationalDatabase().getMinLedgerSeq(); } +std::optional +LedgerMaster::txnIdFromIndex(uint32_t ledgerSeq, uint32_t txnIndex) +{ + uint32_t first = 0, last = 0; + + if (!getValidatedRange(first, last) || last < ledgerSeq) + return {}; + + auto const lgr = getLedgerBySeq(ledgerSeq); + if (!lgr || lgr->txs.empty()) + return {}; + + 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) + return it->first->getTransactionID(); + + return {}; +} + } // namespace ripple diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index ed103ec7fca..df758646540 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1096,7 +1096,11 @@ class RPCParser 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/protocol/ErrorCodes.h b/src/ripple/protocol/ErrorCodes.h index 14166358f8c..87323b0dea8 100644 --- a/src/ripple/protocol/ErrorCodes.h +++ b/src/ripple/protocol/ErrorCodes.h @@ -47,8 +47,8 @@ enum error_code_i { rpcJSON_RPC = 2, rpcFORBIDDEN = 3, + rpcWRONG_NETWORK = 4, // Misc failure - // unused 4, // unused 5, rpcNO_PERMISSION = 6, rpcNO_EVENTS = 7, diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index c7faa4ff98b..2a7e2b14e82 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -226,6 +226,7 @@ JSS(converge_time_s); // out: NetworkOPs JSS(cookie); // out: NetworkOPs JSS(count); // in: AccountTx*, ValidatorList JSS(counters); // in/out: retrieve counters +JSS(ctid); // in/out: Tx RPC JSS(currency_a); // out: BookChanges JSS(currency_b); // out: BookChanges JSS(currentShard); // out: NodeToShardStatus diff --git a/src/ripple/rpc/CTID.h b/src/ripple/rpc/CTID.h new file mode 100644 index 00000000000..8f6c64bc028 --- /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 +#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 > 0x0FFF'FFFF) + return {}; + + uint64_t ctidValue = + ((0xC000'0000ULL + 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) + { + std::string const ctidString(ctid); + + if (ctidString.length() != 16) + return {}; + + if (!boost::regex_match(ctidString, boost::regex("^[0-9A-F]+$"))) + return {}; + + ctidValue = std::stoull(ctidString, nullptr, 16); + } + else if constexpr (std::is_integral_v) + ctidValue = ctid; + else + return {}; + + if ((ctidValue & 0xF000'0000'0000'0000ULL) != 0xC000'0000'0000'0000ULL) + return {}; + + uint32_t ledger_seq = (ctidValue >> 32) & 0xFFFF'FFFUL; + 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 e79997ec8f1..c8b00b96fa4 100644 --- a/src/ripple/rpc/handlers/Tx.cpp +++ b/src/ripple/rpc/handlers/Tx.cpp @@ -25,18 +25,17 @@ #include #include #include +#include #include #include #include #include #include +#include +#include namespace ripple { -// { -// transaction: -// } - static bool isValidated(LedgerMaster& ledgerMaster, std::uint32_t seq, uint256 const& hash) { @@ -54,12 +53,14 @@ struct TxResult Transaction::pointer txn; std::variant, Blob> meta; bool validated = false; + std::optional ctid; TxSearched searchedAll; }; struct TxArgs { - uint256 hash; + std::optional hash; + std::optional> ctid; bool binary = false; std::optional> ledgerRange; }; @@ -73,11 +74,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."}}; + 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; @@ -127,7 +136,7 @@ doTxPostgres(RPC::Context& context, TxArgs const& args) else { res.meta = std::make_shared( - args.hash, res.txn->getLedger(), *meta); + *(args.hash), res.txn->getLedger(), *meta); } res.validated = true; return {res, rpcSUCCESS}; @@ -168,7 +177,7 @@ doTxPostgres(RPC::Context& context, TxArgs const& args) } std::pair -doTxHelp(RPC::Context& context, TxArgs const& args) +doTxHelp(RPC::Context& context, TxArgs args) { if (context.app.config().reporting()) return doTxPostgres(context, args); @@ -196,15 +205,28 @@ doTxHelp(RPC::Context& context, TxArgs const& args) std::pair, std::shared_ptr>; result.searchedAll = TxSearched::unknown; - std::variant v; + + if (args.ctid) + { + 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.hash) + return {result, rpcTXN_NOT_FOUND}; + if (args.ledgerRange) { - v = context.app.getMasterTransaction().fetch(args.hash, range, ec); + v = context.app.getMasterTransaction().fetch(*(args.hash), range, ec); } else { - v = context.app.getMasterTransaction().fetch(args.hash, ec); + v = context.app.getMasterTransaction().fetch(*(args.hash), ec); } if (auto e = std::get_if(&v)) @@ -246,6 +268,15 @@ doTxHelp(RPC::Context& context, TxArgs const& args) } result.validated = isValidated( context.ledgerMaster, ledger->info().seq, ledger->info().hash); + + // compute outgoing CTID + uint32_t lgrSeq = ledger->info().seq; + uint32_t txnIdx = meta->getAsObject().getFieldU32(sfTransactionIndex); + uint32_t netID = context.app.config().NETWORK_ID; + + if (txnIdx <= 0xFFFFU && netID < 0xFFFFU && lgrSeq < 0x0FFF'FFFFUL) + result.ctid = + RPC::encodeCTID(lgrSeq, (uint16_t)txnIdx, (uint16_t)netID); } return {result, rpcSUCCESS}; @@ -301,6 +332,9 @@ populateJsonResponse( } } response[jss::validated] = result.validated; + + if (result.ctid) + response[jss::ctid] = *(result.ctid); } return response; } @@ -313,13 +347,39 @@ doTxJson(RPC::JsonContext& context) // Deserialize and validate JSON arguments - if (!context.params.isMember(jss::transaction)) + TxArgs args; + + if (context.params.isMember(jss::transaction) && + context.params.isMember(jss::ctid)) + // specifying both is ambiguous return rpcError(rpcINVALID_PARAMS); - TxArgs args; + 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 = RPC::decodeCTID(context.params[jss::ctid].asString()); + if (!ctid) + return rpcError(rpcINVALID_PARAMS); - if (!args.hash.parseHex(context.params[jss::transaction].asString())) - return rpcError(rpcNOT_IMPL); + auto const [lgr_seq, txn_idx, net_id] = *ctid; + 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()); + } + args.ctid = {lgr_seq, txn_idx}; + } + else + return rpcError(rpcINVALID_PARAMS); args.binary = context.params.isMember(jss::binary) && context.params[jss::binary].asBool(); diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index bc38df62fc9..7ad7fda4940 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -34,6 +34,7 @@ #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 00000000000..87639c42fcd --- /dev/null +++ b/src/test/app/LedgerMaster_test.cpp @@ -0,0 +1,139 @@ +//------------------------------------------------------------------------------ +/* + 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 (first tx) + { + uint32_t txnIndex = metas[0]->getFieldU32(sfTransactionIndex); + auto result = env.app().getLedgerMaster().txnIdFromIndex( + startLegSeq, txnIndex); + BEAST_EXPECT( + *result == + uint256("277F4FD89C20B92457FEF05FF63F6405563AD0563C73D967A29727" + "72679ADC65")); + } + // success (second tx) + { + uint32_t txnIndex = metas[1]->getFieldU32(sfTransactionIndex); + auto result = env.app().getLedgerMaster().txnIdFromIndex( + startLegSeq + 1, txnIndex); + BEAST_EXPECT( + *result == + uint256("293DF7335EBBAF4420D52E70ABF470EB4C5792CAEA2F91F76193C2" + "819F538FDE")); + } + } + +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/app/NetworkID_test.cpp b/src/test/app/NetworkID_test.cpp index e650667f842..8d1b891345d 100644 --- a/src/test/app/NetworkID_test.cpp +++ b/src/test/app/NetworkID_test.cpp @@ -68,14 +68,9 @@ class NetworkID_test : public beast::unit_test::suite jv[jss::Destination] = alice.human(); jv[jss::TransactionType] = "Payment"; jv[jss::Amount] = "10000000000"; - if (env.app().config().NETWORK_ID > 1024) - jv[jss::NetworkID] = - std::to_string(env.app().config().NETWORK_ID); - env(jv, fee(1000), sig(env.master)); } - // run tx env(jv, fee(1000), ter(expectedOutcome)); env.close(); }; @@ -127,12 +122,29 @@ class NetworkID_test : public beast::unit_test::suite { test::jtx::Env env{*this, makeNetworkConfig(1025)}; BEAST_EXPECT(env.app().config().NETWORK_ID == 1025); + { + env.fund(XRP(200), alice); + // try to submit a txn without network id, this should not work + Json::Value jvn; + jvn[jss::Account] = alice.human(); + jvn[jss::TransactionType] = jss::AccountSet; + jvn[jss::Fee] = to_string(env.current()->fees().base); + jvn[jss::Sequence] = env.seq(alice); + jvn[jss::LastLedgerSequence] = env.current()->info().seq + 2; + auto jt = env.jtnofill(jvn); + Serializer s; + jt.stx->add(s); + BEAST_EXPECT( + env.rpc( + "submit", + strHex(s.slice()))[jss::result][jss::engine_result] == + "telREQUIRES_NETWORK_ID"); + env.close(); + } - // try to submit a txn without network id, this should not work Json::Value jv; jv[jss::Account] = alice.human(); jv[jss::TransactionType] = jss::AccountSet; - runTx(env, jv, telREQUIRES_NETWORK_ID); // try to submit with wrong network id jv[jss::NetworkID] = 0; diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 2a85a57e1db..57e69ef79df 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -445,6 +445,18 @@ class Env return jt; } + /** Create a JTx from parameters. */ + template + JTx + jtnofill(JsonValue&& jv, FN const&... fN) + { + JTx jt(std::forward(jv)); + invoke(jt, fN...); + autofill_sig(jt); + jt.stx = st(jt); + return jt; + } + /** Create JSON from parameters. This will apply funclets and autofill. */ diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index e0126d86854..5e1aaa166f0 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -415,6 +415,11 @@ Env::autofill(JTx& jt) jtx::fill_fee(jv, *current()); if (jt.fill_seq) jtx::fill_seq(jv, *current()); + + uint32_t networkID = app().config().NETWORK_ID; + if (!jv.isMember(jss::NetworkID) && networkID > 1024) + jv[jss::NetworkID] = std::to_string(networkID); + // Must come last try { diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index 1ae15afa303..5385ba768e6 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -5593,6 +5593,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"}, @@ -6379,6 +6410,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: @@ -6387,7 +6428,8 @@ class RPCCall_test : public beast::unit_test::suite { 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 08e97c1c20a..cc3f70a1516 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,423 @@ 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 < 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); + + 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(0x0FFF'FFFFUL, 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(0x1000'0000UL, 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(0x0FFF'FFFF, (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(0x0FFF'FFFFUL, 0xFFFFU, (uint16_t)0x1000'0U) == + 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(0xCFFF'FFFF'FFFF'FFFFULL) == + std::optional>( + std::make_tuple(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU)))); + BEAST_EXPECT( + (RPC::decodeCTID(0xC000'0000'0000'0000ULL) == + std::optional>( + std::make_tuple(0, 0, 0)))); + BEAST_EXPECT( + (RPC::decodeCTID(0xC000'0001'0002'0003ULL) == + std::optional>( + std::make_tuple(1U, 2U, 3U)))); + BEAST_EXPECT( + (RPC::decodeCTID(0xC0CA'2AA7'326F'C045ULL) == + std::optional>( + std::make_tuple(1324'9191UL, 12911U, 49221U)))); + + // Test case 12: ctid not exactly 16 nibbles + BEAST_EXPECT(!RPC::decodeCTID(0xC003'FFFF'FFFF'FFF)); + + // 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(0xFFFF'FFFF'FFFF'FFFFULL)); + } + + 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); } };