From 8fea47447c7de4eb1cb5c2f1f79cd6ebd8bef265 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Tue, 11 Jun 2024 19:34:02 +0100 Subject: [PATCH] Add new command line option to make replaying transactions easier: (#5027) * Add trap_tx_hash command line option This new option can be used only if replay is also enabled. It takes a transaction hash from the ledger loaded for replay, and will cause a specific line to be hit in Transactor.cpp, right before the selected transaction is applied. --- src/ripple/app/main/Application.cpp | 29 ++++- src/ripple/app/main/Application.h | 3 + src/ripple/app/main/Main.cpp | 30 +++++ src/ripple/app/tx/impl/Transactor.cpp | 6 + src/ripple/core/Config.h | 2 + src/test/app/LedgerLoad_test.cpp | 152 ++++++++++++++++++++++++-- 6 files changed, 211 insertions(+), 11 deletions(-) diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index ddc744c3f..c9d6f3190 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -228,6 +228,7 @@ public: std::unique_ptr mRelationalDatabase; std::unique_ptr mWalletDB; std::unique_ptr overlay_; + std::optional trapTxID_; boost::asio::signal_set m_signals; @@ -1257,6 +1258,12 @@ public: return maxDisallowedLedger_; } + virtual const std::optional& + trapTxID() const override + { + return trapTxID_; + } + private: // For a newly-started validator, this is the greatest persisted ledger // and new validations must be greater than this. @@ -1282,7 +1289,8 @@ private: std::string const& ledgerID, bool replay, bool isFilename, - bool isJson); + bool isJson, + std::optional trapTxID); void setMaxDisallowedLedger(); @@ -1415,7 +1423,8 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) config_->START_LEDGER, startUp == Config::REPLAY, startUp == Config::LOAD_FILE, - startUp == Config::LOAD_JSON)) + startUp == Config::LOAD_JSON, + config_->TRAP_TX_HASH)) { JLOG(m_journal.error()) << "The specified ledger could not be loaded."; @@ -2265,7 +2274,8 @@ ApplicationImp::loadOldLedger( std::string const& ledgerID, bool replay, bool isFileName, - bool isJson) + bool isJson, + std::optional trapTxID) { try { @@ -2417,6 +2427,11 @@ ApplicationImp::loadOldLedger( { (void)_; auto txID = tx->getTransactionID(); + if (trapTxID == txID) + { + trapTxID_ = txID; + JLOG(m_journal.debug()) << "Trap transaction set: " << txID; + } auto s = std::make_shared(); tx->add(*s); @@ -2431,6 +2446,14 @@ ApplicationImp::loadOldLedger( } m_ledgerMaster->takeReplay(std::move(replayData)); + + if (trapTxID && !trapTxID_) + { + JLOG(m_journal.fatal()) + << "Ledger " << replayLedger->info().seq + << " does not contain the transaction hash " << *trapTxID; + return false; + } } } catch (SHAMapMissingNode const& mn) diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 29ab68b7a..38cbe3f48 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -283,6 +283,9 @@ public: * than the last ledger it persisted. */ virtual LedgerIndex getMaxDisallowedLedger() = 0; + + virtual const std::optional& + trapTxID() const = 0; }; std::unique_ptr diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 0b2beeb47..8516393b7 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -399,6 +399,9 @@ run(int argc, char** argv) "net", "Get the initial ledger from the network.")( "nodetoshard", "Import node store into shards")( "replay", "Replay a ledger close.")( + "trap_tx_hash", + po::value(), + "Trap a specific transaction during replay.")( "start", "Start from a fresh Ledger.")( "startReporting", po::value(), @@ -557,6 +560,7 @@ run(int argc, char** argv) argc, argv); } + // LCOV_EXCL_START else { if (vm.count("unittest-jobs")) @@ -678,7 +682,25 @@ run(int argc, char** argv) { config->START_LEDGER = vm["ledger"].as(); if (vm.count("replay")) + { config->START_UP = Config::REPLAY; + if (vm.count("trap_tx_hash")) + { + uint256 tmp = {}; + auto hash = vm["trap_tx_hash"].as(); + if (tmp.parseHex(hash)) + { + config->TRAP_TX_HASH = tmp; + } + else + { + std::cerr << "Trap parameter was ill-formed, expected " + "valid transaction hash but received: " + << hash << std::endl; + return -1; + } + } + } else config->START_UP = Config::LOAD; } @@ -692,6 +714,13 @@ run(int argc, char** argv) config->START_UP = Config::LOAD; } + if (vm.count("trap_tx_hash") && vm.count("replay") == 0) + { + std::cerr << "Cannot use trap option without replay option" + << std::endl; + return -1; + } + if (vm.count("net") && !config->FAST_LOAD) { if ((config->START_UP == Config::LOAD) || @@ -827,6 +856,7 @@ run(int argc, char** argv) beast::setCurrentThreadName("xahaud: rpc"); return RPCCall::fromCommandLine( *config, vm["parameters"].as>(), *logs); + // LCOV_EXCL_STOP } } // namespace ripple diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 1b08ab615..403ce2bb5 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -1792,6 +1792,12 @@ Transactor::operator()() SF_EMITTED))) return {tecINTERNAL, false}; + if (auto const& trap = ctx_.app.trapTxID(); + trap && *trap == ctx_.tx.getTransactionID()) + { + JLOG(j_.debug()) << "Transaction trapped: " << *trap; + } + auto result = ctx_.preclaimResult; bool const hooksEnabled = view().rules().enabled(featureHooks); diff --git a/src/ripple/core/Config.h b/src/ripple/core/Config.h index df16c946a..6aff1d30e 100644 --- a/src/ripple/core/Config.h +++ b/src/ripple/core/Config.h @@ -177,6 +177,8 @@ public: std::string START_LEDGER; + std::optional TRAP_TX_HASH; + // Network parameters uint32_t NETWORK_ID = 0; diff --git a/src/test/app/LedgerLoad_test.cpp b/src/test/app/LedgerLoad_test.cpp index c4cee645e..a5e64a4c9 100644 --- a/src/test/app/LedgerLoad_test.cpp +++ b/src/test/app/LedgerLoad_test.cpp @@ -18,6 +18,8 @@ //============================================================================== #include +#include +#include #include #include #include @@ -36,10 +38,12 @@ class LedgerLoad_test : public beast::unit_test::suite std::unique_ptr cfg, std::string const& dbPath, std::string const& ledger, - Config::StartUpType type) + Config::StartUpType type, + std::optional trapTxHash) { cfg->START_LEDGER = ledger; cfg->START_UP = type; + cfg->TRAP_TX_HASH = trapTxHash; assert(!dbPath.empty()); cfg->legacy("database_path", dbPath); auto& sectionNode = cfg->section(ConfigSection::nodeDatabase()); @@ -55,6 +59,7 @@ class LedgerLoad_test : public beast::unit_test::suite std::string ledgerFile{}; Json::Value ledger{}; Json::Value hashes{}; + uint256 trapTxHash{}; }; SetupData @@ -103,6 +108,16 @@ class LedgerLoad_test : public beast::unit_test::suite }(); BEAST_EXPECT(retval.hashes.size() == 41); + retval.trapTxHash = [&]() { + auto const txs = env.rpc( + "ledger", + std::to_string(41), + "tx")[jss::result][jss::ledger][jss::transactions]; + BEAST_EXPECT(txs.isArray() && txs.size() > 0); + uint256 tmp; + BEAST_EXPECT(tmp.parseHex(txs[0u][jss::hash].asString())); + return tmp; + }(); // write this ledger data to a file. std::ofstream o(retval.ledgerFile, std::ios::out | std::ios::trunc); @@ -121,7 +136,11 @@ class LedgerLoad_test : public beast::unit_test::suite Env env( *this, envconfig( - ledgerConfig, sd.dbPath, sd.ledgerFile, Config::LOAD_FILE), + ledgerConfig, + sd.dbPath, + sd.ledgerFile, + Config::LOAD_FILE, + std::nullopt), nullptr, beast::severities::kDisabled); auto jrb = env.rpc("ledger", "current", "full")[jss::result]; @@ -141,7 +160,12 @@ class LedgerLoad_test : public beast::unit_test::suite except([&] { Env env( *this, - envconfig(ledgerConfig, sd.dbPath, "", Config::LOAD_FILE), + envconfig( + ledgerConfig, + sd.dbPath, + "", + Config::LOAD_FILE, + std::nullopt), nullptr, beast::severities::kDisabled); }); @@ -151,7 +175,11 @@ class LedgerLoad_test : public beast::unit_test::suite Env env( *this, envconfig( - ledgerConfig, sd.dbPath, "badfile.json", Config::LOAD_FILE), + ledgerConfig, + sd.dbPath, + "badfile.json", + Config::LOAD_FILE, + std::nullopt), nullptr, beast::severities::kDisabled); }); @@ -181,7 +209,8 @@ class LedgerLoad_test : public beast::unit_test::suite ledgerConfig, sd.dbPath, ledgerFileCorrupt.string(), - Config::LOAD_FILE), + Config::LOAD_FILE, + std::nullopt), nullptr, beast::severities::kDisabled); }); @@ -198,7 +227,12 @@ class LedgerLoad_test : public beast::unit_test::suite boost::erase_all(ledgerHash, "\""); Env env( *this, - envconfig(ledgerConfig, sd.dbPath, ledgerHash, Config::LOAD), + envconfig( + ledgerConfig, + sd.dbPath, + ledgerHash, + Config::LOAD, + std::nullopt), nullptr, beast::severities::kDisabled); auto jrb = env.rpc("ledger", "current", "full")[jss::result]; @@ -208,6 +242,103 @@ class LedgerLoad_test : public beast::unit_test::suite sd.ledger[jss::ledger][jss::accountState].size()); } + void + testReplay(SetupData const& sd) + { + testcase("Load and replay by hash"); + using namespace test::jtx; + + // create a new env with the ledger hash specified for startup + auto ledgerHash = to_string(sd.hashes[sd.hashes.size() - 1]); + boost::erase_all(ledgerHash, "\""); + Env env( + *this, + envconfig( + ledgerConfig, + sd.dbPath, + ledgerHash, + Config::REPLAY, + std::nullopt), + nullptr, + beast::severities::kDisabled); + auto const jrb = env.rpc("ledger", "current", "full")[jss::result]; + BEAST_EXPECT(jrb[jss::ledger][jss::accountState].size() == 97); + // in replace mode do not automatically accept the ledger being replayed + + env.close(); + auto const closed = env.rpc("ledger", "current", "full")[jss::result]; + BEAST_EXPECT(closed[jss::ledger][jss::accountState].size() == 98); + BEAST_EXPECT( + closed[jss::ledger][jss::accountState].size() <= + sd.ledger[jss::ledger][jss::accountState].size()); + } + + void + testReplayTx(SetupData const& sd) + { + testcase("Load and replay transaction by hash"); + using namespace test::jtx; + + // create a new env with the ledger hash specified for startup + auto ledgerHash = to_string(sd.hashes[sd.hashes.size() - 1]); + boost::erase_all(ledgerHash, "\""); + Env env( + *this, + envconfig( + ledgerConfig, + sd.dbPath, + ledgerHash, + Config::REPLAY, + sd.trapTxHash), + nullptr, + beast::severities::kDisabled); + auto const jrb = env.rpc("ledger", "current", "full")[jss::result]; + BEAST_EXPECT(jrb[jss::ledger][jss::accountState].size() == 97); + // in replace mode do not automatically accept the ledger being replayed + + env.close(); + auto const closed = env.rpc("ledger", "current", "full")[jss::result]; + BEAST_EXPECT(closed[jss::ledger][jss::accountState].size() == 98); + BEAST_EXPECT( + closed[jss::ledger][jss::accountState].size() <= + sd.ledger[jss::ledger][jss::accountState].size()); + } + + void + testReplayTxFail(SetupData const& sd) + { + testcase("Load and replay transaction by hash failure"); + using namespace test::jtx; + + // create a new env with the ledger hash specified for startup + auto ledgerHash = to_string(sd.hashes[sd.hashes.size() - 1]); + boost::erase_all(ledgerHash, "\""); + try + { + // will throw an exception, because we cannot load a ledger for + // replay when trapTxHash is set to an invalid transaction + Env env( + *this, + envconfig( + ledgerConfig, + sd.dbPath, + ledgerHash, + Config::REPLAY, + ~sd.trapTxHash), + nullptr, + beast::severities::kDisabled); + BEAST_EXPECT(false); + } + catch (std::runtime_error const&) + { + BEAST_EXPECT(true); + } + catch (...) + { + BEAST_EXPECT(false); + } + } + void testLoadLatest(SetupData const& sd) { @@ -217,7 +348,8 @@ class LedgerLoad_test : public beast::unit_test::suite // create a new env with the ledger "latest" specified for startup Env env( *this, - envconfig(ledgerConfig, sd.dbPath, "latest", Config::LOAD), + envconfig( + ledgerConfig, sd.dbPath, "latest", Config::LOAD, std::nullopt), nullptr, beast::severities::kDisabled); auto jrb = env.rpc("ledger", "current", "full")[jss::result]; @@ -235,7 +367,8 @@ class LedgerLoad_test : public beast::unit_test::suite // create a new env with specific ledger index at startup Env env( *this, - envconfig(ledgerConfig, sd.dbPath, "43", Config::LOAD), + envconfig( + ledgerConfig, sd.dbPath, "43", Config::LOAD, std::nullopt), nullptr, beast::severities::kDisabled); auto jrb = env.rpc("ledger", "current", "full")[jss::result]; @@ -255,6 +388,9 @@ public: testLoad(sd); testBadFiles(sd); testLoadByHash(sd); + testReplay(sd); + testReplayTx(sd); + testReplayTxFail(sd); testLoadLatest(sd); testLoadIndex(sd); }