diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 55300a390c9..4f994cfe5ac 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -225,6 +225,7 @@ class ApplicationImp : public Application, public BasicApp std::unique_ptr mRelationalDatabase; std::unique_ptr mWalletDB; std::unique_ptr overlay_; + std::optional trapTxID_; boost::asio::signal_set m_signals; @@ -1254,6 +1255,12 @@ class ApplicationImp : public Application, public BasicApp 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. @@ -1272,7 +1279,11 @@ class ApplicationImp : public Application, public BasicApp loadLedgerFromFile(std::string const& ledgerID); bool - loadOldLedger(std::string const& ledgerID, bool replay, bool isFilename); + loadOldLedger( + std::string const& ledgerID, + bool replay, + bool isFilename, + std::optional trapTxID); void setMaxDisallowedLedger(); @@ -1404,7 +1415,8 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) if (!loadOldLedger( config_->START_LEDGER, startUp == Config::REPLAY, - startUp == Config::LOAD_FILE)) + startUp == Config::LOAD_FILE, + config_->TRAP_TX_HASH)) { JLOG(m_journal.error()) << "The specified ledger could not be loaded."; @@ -2086,7 +2098,8 @@ bool ApplicationImp::loadOldLedger( std::string const& ledgerID, bool replay, - bool isFileName) + bool isFileName, + std::optional trapTxID) { try { @@ -2233,6 +2246,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); @@ -2247,6 +2265,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 3fa8d13e870..4a7d72aec25 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -284,6 +284,9 @@ class Application : public beast::PropertyStream::Source * 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 710e4e9674f..be4e354b6aa 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -400,6 +400,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(), @@ -558,6 +561,7 @@ run(int argc, char** argv) argc, argv); } + // LCOV_EXCL_START else { if (vm.count("unittest-jobs")) @@ -679,7 +683,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; } @@ -693,6 +715,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) || @@ -828,6 +857,7 @@ run(int argc, char** argv) beast::setCurrentThreadName("rippled: 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 7dcf3f15ab7..c02b57a3ae4 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -854,6 +854,12 @@ Transactor::operator()() } #endif + if (auto const& trap = ctx_.app.trapTxID(); + trap && *trap == ctx_.tx.getTransactionID()) + { + JLOG(j_.debug()) << "Transaction trapped: " << *trap; + } + auto result = ctx_.preclaimResult; if (result == tesSUCCESS) result = apply(); diff --git a/src/ripple/core/Config.h b/src/ripple/core/Config.h index cf41678a16c..24b762cf8c5 100644 --- a/src/ripple/core/Config.h +++ b/src/ripple/core/Config.h @@ -162,6 +162,8 @@ class Config : public BasicConfig 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 f06e7d0bf01..599215c2435 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); return cfg; @@ -52,6 +56,7 @@ class LedgerLoad_test : public beast::unit_test::suite std::string ledgerFile{}; Json::Value ledger{}; Json::Value hashes{}; + uint256 trapTxHash{}; }; SetupData @@ -94,6 +99,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); @@ -112,7 +127,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]; @@ -132,7 +151,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); }); @@ -142,7 +166,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); }); @@ -172,7 +200,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); }); @@ -189,7 +218,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]; @@ -199,6 +233,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) { @@ -208,7 +339,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]; @@ -226,7 +358,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]; @@ -246,6 +379,9 @@ class LedgerLoad_test : public beast::unit_test::suite testLoad(sd); testBadFiles(sd); testLoadByHash(sd); + testReplay(sd); + testReplayTx(sd); + testReplayTxFail(sd); testLoadLatest(sd); testLoadIndex(sd); }