diff --git a/include/xrpl/core/Job.h b/include/xrpl/core/Job.h index 61651fd6738..2d1421674d5 100644 --- a/include/xrpl/core/Job.h +++ b/include/xrpl/core/Job.h @@ -54,6 +54,7 @@ enum JobType { jtNETOP_CLUSTER, // NetworkOPs cluster peer report jtNETOP_TIMER, // NetworkOPs net timer processing jtADMIN, // An administrative operation + jtWRITE_RPC_DEBUG, // Write RPC debug logs to dbs // Special job types which are not dispatched by the job pool jtPEER, diff --git a/include/xrpl/core/JobTypes.h b/include/xrpl/core/JobTypes.h index 88f98aad663..9af5c227647 100644 --- a/include/xrpl/core/JobTypes.h +++ b/include/xrpl/core/JobTypes.h @@ -77,6 +77,7 @@ class JobTypes add(jtADMIN, "administration", maxLimit, 0ms, 0ms); add(jtMISSING_TXN, "handleHaveTransactions", 1200, 0ms, 0ms); add(jtREQUESTED_TXN, "doTransactions", 1200, 0ms, 0ms); + add(jtWRITE_RPC_DEBUG, "writeRPCDebug", maxLimit, 0ms, 0ms); add(jtPEER, "peerCommand", 0, 200ms, 2500ms); add(jtDISK, "diskAccess", 0, 500ms, 1000ms); diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 05942353dd4..c47fc92898c 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -241,6 +241,7 @@ JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit JSS(entire_set); // out: get_aggregate_price +JSS(entry_id); // out: tx JSS(ephemeral_key); // out: ValidatorInfo // in/out: Manifest JSS(error); // out: error @@ -608,6 +609,7 @@ JSS(total); // out: counters JSS(total_bytes_recv); // out: Peers JSS(total_bytes_sent); // out: Peers JSS(total_coins); // out: LedgerToJson +JSS(traces); // out: tx JSS(trading_fee); // out: amm_info JSS(transTreeHash); // out: ledger/Ledger.cpp JSS(transaction); // in: Tx @@ -692,6 +694,7 @@ JSS(vote_slots); // out: amm_info JSS(vote_weight); // out: amm_info JSS(warning); // rpc: JSS(warnings); // out: server_info, server_state +JSS(wasm_traces); // out: tx JSS(workers); JSS(write_load); // out: GetCounts // clang-format on diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 2c0d3c2b822..c058ffb0ee0 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -194,6 +195,7 @@ class ApplicationImp : public Application, public BasicApp std::unique_ptr mRelationalDatabase; std::unique_ptr mWalletDB; + std::unique_ptr mWasmTraceDB; std::unique_ptr overlay_; std::optional trapTxID_; @@ -744,6 +746,16 @@ class ApplicationImp : public Application, public BasicApp return *mWalletDB; } + DatabaseCon& + getWasmTraceDB() override + { + XRPL_ASSERT( + mWasmTraceDB, + "xrpl::ApplicationImp::getWasmTraceDB : null wasm debug " + "database"); + return *mWasmTraceDB; + } + bool serverOkay(std::string& reason) override; @@ -759,16 +771,25 @@ class ApplicationImp : public Application, public BasicApp mWalletDB.get() == nullptr, "xrpl::ApplicationImp::initRelationalDatabase : null wallet " "database"); + XRPL_ASSERT( + mWasmTraceDB.get() == nullptr, + "xrpl::ApplicationImp::initRelationalDatabase : null wasm debug " + "database"); try { mRelationalDatabase = RelationalDatabase::init(*this, *config_, *m_jobQueue); - // wallet database auto setup = setup_DatabaseCon(*config_, m_journal); setup.useGlobalPragma = false; + // wallet database mWalletDB = makeWalletDB(setup, m_journal); + if (config().useTxTables()) + { + // wasm debug database + mWasmTraceDB = makeWasmTraceDB(setup, m_journal); + } } catch (std::exception const& e) { diff --git a/src/xrpld/app/main/Application.h b/src/xrpld/app/main/Application.h index 53cc264ad4b..b2b71f0de80 100644 --- a/src/xrpld/app/main/Application.h +++ b/src/xrpld/app/main/Application.h @@ -169,6 +169,10 @@ class Application : public ServiceRegistry, public beast::PropertyStream::Source virtual DatabaseCon& getWalletDB() = 0; + /** Retrieve the "WASM debug database" */ + virtual DatabaseCon& + getWasmTraceDB() = 0; + /** Ensure that a newly-started validator does not sign proposals older * than the last ledger it persisted. */ virtual LedgerIndex diff --git a/src/xrpld/app/main/DBInit.h b/src/xrpld/app/main/DBInit.h index 56bc7523431..77428b2546d 100644 --- a/src/xrpld/app/main/DBInit.h +++ b/src/xrpld/app/main/DBInit.h @@ -115,4 +115,18 @@ inline constexpr std::array WalletDBInit{ "END TRANSACTION;"}}; +inline constexpr auto WasmTraceDBName{"wasm_trace.db"}; + +inline constexpr std::array WasmTraceDBInit{ + {"BEGIN TRANSACTION;", + + "CREATE TABLE IF NOT EXISTS WasmTraceLogs (" + " TransID CHARACTER(64) NOT NULL," + " ObjID CHARACTER(64) NOT NULL," + " Data TEXT NOT NULL," + " PRIMARY KEY(TransID, ObjID)" + ");", + + "END TRANSACTION;"}}; + } // namespace xrpl diff --git a/src/xrpld/app/rdb/WasmTrace.h b/src/xrpld/app/rdb/WasmTrace.h new file mode 100644 index 00000000000..301f819a5fc --- /dev/null +++ b/src/xrpld/app/rdb/WasmTrace.h @@ -0,0 +1,24 @@ +#ifndef XRPL_APP_RDB_WasmTrace_H_INCLUDED +#define XRPL_APP_RDB_WasmTrace_H_INCLUDED + +#include +#include +#include +#include + +#include + +namespace xrpl { + +std::unique_ptr +makeWasmTraceDB(DatabaseCon::Setup const& setup, beast::Journal j); + +void +addWasmTraceLogs(soci::session& session, TxID const& txId, Keylet const& keylet, std::vector const& data); + +std::map> +getWasmTraceByTxID(soci::session& session, TxID const& txId); + +} // namespace xrpl + +#endif diff --git a/src/xrpld/app/rdb/detail/WasmTrace.cpp b/src/xrpld/app/rdb/detail/WasmTrace.cpp new file mode 100644 index 00000000000..262cb7c4961 --- /dev/null +++ b/src/xrpld/app/rdb/detail/WasmTrace.cpp @@ -0,0 +1,66 @@ +#include + +#include + +namespace xrpl { + +std::unique_ptr +makeWasmTraceDB(DatabaseCon::Setup const& setup, beast::Journal j) +{ + // WASM debug log database + return std::make_unique(setup, WasmTraceDBName, std::array(), WasmTraceDBInit, j); +} + +void +addWasmTraceLogs(soci::session& session, TxID const& txId, Keylet const& keylet, std::vector const& data) +{ + soci::transaction tr(session); + + // Convert all the info to appropriate formats + std::string const txHex = to_string(txId); + std::string const keyletHex = to_string(keylet.key); + std::string const logString = boost::algorithm::join(data, "\x1F"); + + // replace = because you run transactions twice: open _and_ closed ledger + session << "INSERT OR REPLACE INTO WasmTraceLogs " + "(TransID, ObjID, Data) VALUES " + "(:transID, :objId, :data)", + soci::use(txHex), soci::use(keyletHex), soci::use(logString); + + tr.commit(); +} + +std::map> +getWasmTraceByTxID(soci::session& session, TxID const& txId) +{ + std::map> ret; + + std::string const txHex = to_string(txId); + + std::string objHex; + std::string logString; + + soci::statement st = + (session.prepare << "SELECT ObjID, Data FROM WasmTraceLogs " + "WHERE TransID = :txId", + soci::use(txHex), + soci::into(objHex), + soci::into(logString)); + + st.execute(); + + while (st.fetch()) + { + uint256 objId; + if (objId.parseHex(objHex)) + { + std::vector logs; + boost::algorithm::split(logs, logString, boost::is_any_of("\x1F")); + ret.emplace(objId, logs); + } + } + + return ret; +} + +} // namespace xrpl diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index bfce567f555..c4423022afb 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -17,6 +18,8 @@ #include #include +#include + namespace xrpl { // During an EscrowFinish, the transaction must specify both @@ -1146,6 +1149,29 @@ EscrowFinish::doApply() std::uint32_t allowance = ctx_.tx[sfComputationAllowance]; auto re = runEscrowWasm(wasm, ledgerDataProvider, ESCROW_FUNCTION_NAME, {}, allowance); JLOG(j_.trace()) << "Escrow WASM ran"; + auto const& logs = ledgerDataProvider.getLogs(); + if (ctx_.app.config().useTxTables() && !logs.empty()) + { + // Capture by value to avoid lifetime issues + auto txId = ctx_.tx.getTransactionID(); + auto keylet = k; + auto logsCopy = logs; + + ctx_.app.getJobQueue().addJob( + jtWRITE_RPC_DEBUG, "writeWasmTrace", [&app = ctx_.app, txId, keylet, logsCopy = std::move(logsCopy)]() { + try + { + auto db = app.getWasmTraceDB().checkoutDb(); + addWasmTraceLogs(*db, txId, keylet, logsCopy); + } + catch (std::exception const& e) + { + // Log error but don't crash - debug logs are + // non-critical + JLOG(app.journal("WasmTrace").warn()) << "Failed to write WASM debug logs: " << e.what(); + } + }); + } if (auto const& data = ledgerDataProvider->getData(); data.has_value()) { diff --git a/src/xrpld/app/wasm/HostFuncImpl.h b/src/xrpld/app/wasm/HostFuncImpl.h index d8e080a8ef9..ff9b766520b 100644 --- a/src/xrpld/app/wasm/HostFuncImpl.h +++ b/src/xrpld/app/wasm/HostFuncImpl.h @@ -10,6 +10,7 @@ class WasmHostFunctionsImpl : public HostFunctions Keylet leKey; std::shared_ptr currentLedgerObj = nullptr; bool isLedgerObjCached = false; + std::vector logs_; static int constexpr MAX_CACHE = 256; std::array, MAX_CACHE> cache; @@ -52,11 +53,13 @@ class WasmHostFunctionsImpl : public HostFunctions return; auto j = getJournal().trace(); #endif - j << "WasmTrace[" << to_short_string(leKey.key) << "]: " << msg << " " << dataFn(); + auto const data = dataFn(); + j << "WasmTrace[" << to_short_string(leKey.key) << "]: " << msg << " " << data; #ifdef DEBUG_OUTPUT j << std::endl; #endif + logs_.emplace_back(std::string(msg) + " " + data); } public: @@ -82,6 +85,12 @@ class WasmHostFunctionsImpl : public HostFunctions return data_; } + std::vector const& + getLogs() const + { + return logs_; + } + Expected getLedgerSqn() override; diff --git a/src/xrpld/rpc/handlers/Tx.cpp b/src/xrpld/rpc/handlers/Tx.cpp index 5d8778d6194..5c2010bb5d7 100644 --- a/src/xrpld/rpc/handlers/Tx.cpp +++ b/src/xrpld/rpc/handlers/Tx.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ struct TxResult std::optional closeTime; std::optional ledgerHash; TxSearched searchedAll; + std::map> WasmTraceLogs; }; struct TxArgs @@ -153,6 +155,13 @@ doTxHelp(RPC::Context& context, TxArgs args) } } + if (txn->getSTransaction()->isFieldPresent(sfComputationAllowance)) + { + auto db = context.app.getWasmTraceDB().checkoutDb(); + auto const logs = getWasmTraceByTxID(*db, txn->getID()); + result.WasmTraceLogs = logs; + } + return {result, rpcSUCCESS}; } @@ -234,6 +243,22 @@ populateJsonResponse(std::pair const& res, TxArgs const& if (result.ctid) response[jss::ctid] = *(result.ctid); + + if (!result.WasmTraceLogs.empty()) + { + response[jss::wasm_traces] = Json::Value(Json::arrayValue); + for (auto const& [entryId, logs] : result.WasmTraceLogs) + { + Json::Value logEntry = Json::objectValue; + logEntry[jss::entry_id] = to_string(entryId); + logEntry[jss::traces] = Json::arrayValue; + for (auto& log : logs) + { + logEntry[jss::traces].append(log); + } + response[jss::wasm_traces].append(logEntry); + } + } } return response; }