From dae082d6a59330827fcc2c990d239cdebde8bdf8 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Thu, 22 Jan 2026 16:42:05 +0700 Subject: [PATCH] chore: format files with clang-format --- src/ripple/app/consensus/RCLConsensus.cpp | 50 +++++++- src/ripple/app/hook/impl/applyHook.cpp | 3 + src/ripple/app/ledger/impl/OpenLedger.cpp | 29 +++++ src/ripple/app/misc/NetworkOPs.cpp | 54 ++++++++- src/ripple/app/misc/NetworkOPs.h | 4 +- src/ripple/app/misc/impl/TxQ.cpp | 132 +++++++++++++++------ src/ripple/app/tx/impl/ExportSign.cpp | 2 +- src/ripple/ledger/impl/OpenView.cpp | 29 ++++- src/ripple/protocol/impl/LedgerFormats.cpp | 2 + src/ripple/protocol/impl/STObject.cpp | 11 ++ src/ripple/rpc/handlers/LedgerAccept.cpp | 2 +- src/test/app/Export_test.cpp | 69 +++++++++-- src/test/jtx/impl/Env.cpp | 3 +- 13 files changed, 336 insertions(+), 54 deletions(-) diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 571e5df26..71d5e8afe 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,17 @@ #include #include +// Debug macro for export investigation - remove after debugging +#include +#define DBG_EXPORT(msg) \ + do \ + { \ + std::cerr << "[" << __FILE__ << ":" << __LINE__ \ + << " t=" << std::this_thread::get_id() << "] " << msg \ + << std::endl; \ + std::cerr.flush(); \ + } while (0) + namespace ripple { RCLConsensus::RCLConsensus( @@ -323,8 +335,10 @@ RCLConsensus::Adaptor::onClose( initialSet->setUnbacked(); // Build SHAMap containing all transactions in our open ledger + DBG_EXPORT("onClose: iterating initialLedger->txs"); for (auto const& tx : initialLedger->txs) { + DBG_EXPORT("onClose: processing tx " << tx.first->getTransactionID()); JLOG(j_.trace()) << "Adding open ledger TX " << tx.first->getTransactionID(); Serializer s(2048); @@ -333,6 +347,7 @@ RCLConsensus::Adaptor::onClose( SHAMapNodeType::tnTRANSACTION_NM, make_shamapitem(tx.first->getTransactionID(), s.slice())); } + DBG_EXPORT("onClose: done iterating initialLedger->txs"); // Add pseudo-transactions to the set if (app_.config().standalone() || (proposing && !wrongLCL)) @@ -406,6 +421,7 @@ RCLConsensus::Adaptor::onForceAccept( ConsensusMode const& mode, Json::Value&& consensusJson) { + DBG_EXPORT("onForceAccept prevLedger.seq=" << prevLedger.seq()); doAccept( result, prevLedger, @@ -424,6 +440,7 @@ RCLConsensus::Adaptor::onAccept( ConsensusMode const& mode, Json::Value&& consensusJson) { + DBG_EXPORT("onAccept (async job) prevLedger.seq=" << prevLedger.seq()); app_.getJobQueue().addJob( jtACCEPT, "acceptLedger", @@ -453,6 +470,7 @@ RCLConsensus::Adaptor::doAccept( ConsensusMode const& mode, Json::Value&& consensusJson) { + DBG_EXPORT("doAccept prevLedger.seq=" << prevLedger.seq()); prevProposers_ = result.proposers; prevRoundTime_ = result.roundTime.read(); @@ -653,14 +671,40 @@ RCLConsensus::Adaptor::doAccept( tapNONE, "consensus", [&](OpenView& view, beast::Journal j) { + DBG_EXPORT("consensus callback seq=" << view.info().seq); //@@start export-sign-submit - // Generate and submit ttEXPORT_SIGN UVTxns if we're a - // validator on the UNLReport + // Generate ttEXPORT_SIGN UVTxns if we're a validator on the + // UNLReport. In standalone mode we queue via rawTxInsert so + // it's applied when this open ledger closes. In network mode + // we submit for relay to other validators. if (view.rules().enabled(featureExport)) { auto exportSignTxns = makeExportSignTxns(view, app_, j_); for (auto const& tx : exportSignTxns) - app_.getOPs().submitTransaction(tx); + { + uint256 txID = tx->getTransactionID(); + app_.getHashRouter().setFlags(txID, SF_PRIVATE2); + + if (app_.config().standalone()) + { + // Standalone: queue in open ledger, applied when it + // closes + auto s = std::make_shared(); + tx->add(*s); + view.rawTxInsert(txID, std::move(s), nullptr); + DBG_EXPORT( + "standalone: queued ttEXPORT_SIGN txID=" + << txID); + } + else + { + // Network: submit for relay to other validators + app_.getOPs().submitTransaction(tx); + DBG_EXPORT( + "network: submitted ttEXPORT_SIGN txID=" + << txID); + } + } } //@@end export-sign-submit return app_.getTxQ().accept(app_, view); diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index 31b857aa3..269e08ba4 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -2067,6 +2067,9 @@ hook::finalizeHookResult( if (page) { (*sleExported)[sfOwnerNode] = *page; + (*sleExported)[sfLedgerSequence] = + applyCtx.view().info().seq; + (*sleExported)[sfTransactionHash] = id; applyCtx.view().insert(sleExported); } else diff --git a/src/ripple/app/ledger/impl/OpenLedger.cpp b/src/ripple/app/ledger/impl/OpenLedger.cpp index 58d2f3b9b..007e1639e 100644 --- a/src/ripple/app/ledger/impl/OpenLedger.cpp +++ b/src/ripple/app/ledger/impl/OpenLedger.cpp @@ -74,6 +74,17 @@ OpenLedger::modify(modify_type const& f) } void +// Debug macro for export investigation - remove after debugging +#include +#define DBG_EXPORT(msg) \ + do \ + { \ + std::cerr << "[" << __FILE__ << ":" << __LINE__ \ + << " t=" << std::this_thread::get_id() << "] " << msg \ + << std::endl; \ + std::cerr.flush(); \ + } while (0) + OpenLedger::accept( Application& app, Rules const& rules, @@ -85,6 +96,8 @@ OpenLedger::accept( std::string const& suffix, modify_type const& f) { + DBG_EXPORT( + "OpenLedger::accept seq=" << ledger->seq() << " suffix=" << suffix); JLOG(j_.trace()) << "accept ledger " << ledger->seq() << " " << suffix; auto next = create(rules, ledger); if (retriesFirst) @@ -118,10 +131,23 @@ OpenLedger::accept( // Call the modifier if (f) f(*next, j_); + DBG_EXPORT( + "OpenLedger::accept AFTER CALLBACK seq=" + << next->info().seq << " locals.size()=" << locals.size()); // Apply local tx for (auto const& item : locals) + { + DBG_EXPORT( + "OpenLedger::accept applying local tx seq=" << next->info().seq); app.getTxQ().apply(app, *next, item.second, flags, j_); + } + DBG_EXPORT("OpenLedger::accept AFTER LOCAL TXS seq=" << next->info().seq); + DBG_EXPORT( + "OpenLedger::accept RELAY LOOP DISABLED FOR DEBUGGING seq=" + << next->info().seq); +// COMPLETELY DISABLED FOR DEBUGGING +#if 0 // If we didn't relay this transaction recently, relay it to all peers for (auto const& txpair : next->txs) { @@ -146,10 +172,13 @@ OpenLedger::accept( app.overlay().relay(txId, msg, *toSkip); } } +#endif // Switch to the new open view + DBG_EXPORT("OpenLedger::accept SWITCHING VIEW seq=" << next->info().seq); std::lock_guard lock2(current_mutex_); current_ = std::move(next); + DBG_EXPORT("OpenLedger::accept EXIT"); } //------------------------------------------------------------------------------ diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 0e5b8ef5f..6bb4c2043 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -79,6 +79,18 @@ #include #include +// Debug macro for export investigation - remove after debugging +#include +#include +#define DBG_EXPORT(msg) \ + do \ + { \ + std::cerr << "[" << __FILE__ << ":" << __LINE__ \ + << " t=" << std::this_thread::get_id() << "] " << msg \ + << std::endl; \ + std::cerr.flush(); \ + } while (0) + namespace ripple { class NetworkOPsImp final : public NetworkOPs @@ -360,7 +372,8 @@ public: getLedgerFetchInfo() override; std::uint32_t acceptLedger( - std::optional consensusDelay) override; + std::optional consensusDelay, + std::string const& caller = "unknown") override; void reportFeeChange() override; void @@ -1773,6 +1786,7 @@ NetworkOPsImp::switchLastClosedLedger( tapNONE, "jump", [&](OpenView& view, beast::Journal j) { + DBG_EXPORT("jump callback seq=" << view.info().seq); // Stuff the ledger with transactions from the queue. return app_.getTxQ().accept(app_, view); }); @@ -3949,7 +3963,8 @@ NetworkOPsImp::unsubBook(std::uint64_t uSeq, Book const& book) std::uint32_t NetworkOPsImp::acceptLedger( - std::optional consensusDelay) + std::optional consensusDelay, + std::string const& caller) { // This code-path is exclusively used when the server is in standalone // mode via `ledger_accept` @@ -3959,11 +3974,44 @@ NetworkOPsImp::acceptLedger( Throw( "Operation only possible in STANDALONE mode."); + // Detect reentrant calls + static thread_local int acceptLedgerDepth = 0; + auto closedSeq = m_ledgerMaster.getClosedLedger()->info().seq; + DBG_EXPORT( + "acceptLedger ENTER closedSeq=" + << closedSeq << " depth=" << acceptLedgerDepth << " caller=" << caller); + + if (acceptLedgerDepth > 0) + { + DBG_EXPORT( + "REENTRANT acceptLedger DETECTED! depth=" << acceptLedgerDepth); + // Print stack trace + void* callstack[128]; + int frames = backtrace(callstack, 128); + char** strs = backtrace_symbols(callstack, frames); + std::cerr << "=== STACK TRACE ===" << std::endl; + for (int i = 0; i < frames; ++i) + { + std::cerr << strs[i] << std::endl; + } + free(strs); + std::cerr << "=== END STACK TRACE ===" << std::endl; + std::cerr.flush(); + Throw( + "REENTRANT acceptLedger call detected at depth " + + std::to_string(acceptLedgerDepth)); + } + ++acceptLedgerDepth; + // FIXME Could we improve on this and remove the need for a specialized // API in Consensus? beginConsensus(m_ledgerMaster.getClosedLedger()->info().hash); mConsensus.simulate(app_.timeKeeper().closeTime(), consensusDelay); - return m_ledgerMaster.getCurrentLedger()->info().seq; + + --acceptLedgerDepth; + auto result = m_ledgerMaster.getCurrentLedger()->info().seq; + DBG_EXPORT("acceptLedger EXIT result=" << result); + return result; } // <-- bool: true=added, false=already there diff --git a/src/ripple/app/misc/NetworkOPs.h b/src/ripple/app/misc/NetworkOPs.h index 350542404..3bf39b320 100644 --- a/src/ripple/app/misc/NetworkOPs.h +++ b/src/ripple/app/misc/NetworkOPs.h @@ -219,8 +219,8 @@ public: */ virtual std::uint32_t acceptLedger( - std::optional consensusDelay = - std::nullopt) = 0; + std::optional consensusDelay = std::nullopt, + std::string const& caller = "unknown") = 0; virtual void reportFeeChange() = 0; diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 25665cb5c..a61e9cf5a 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -33,6 +33,17 @@ #include #include +// Debug macro for export investigation - remove after debugging +#include +#define DBG_EXPORT(msg) \ + do \ + { \ + std::cerr << "[" << __FILE__ << ":" << __LINE__ \ + << " t=" << std::this_thread::get_id() << "] " << msg \ + << std::endl; \ + std::cerr.flush(); \ + } while (0) + namespace ripple { ////////////////////////////////////////////////////////////////////////// @@ -1446,13 +1457,17 @@ TxQ::accept(Application& app, OpenView& view) Stop when the transaction fee level gets lower than the required fee level. */ + DBG_EXPORT("TxQ::accept ENTER seq=" << view.info().seq); auto ledgerChanged = false; + DBG_EXPORT("TxQ::accept BEFORE LOCK seq=" << view.info().seq); std::lock_guard lock(mutex_); + DBG_EXPORT("TxQ::accept AFTER LOCK seq=" << view.info().seq); auto const metricsSnapshot = feeMetrics_.getSnapshot(); + DBG_EXPORT("TxQ::accept SECTION 1: debug inject seq=" << view.info().seq); // try to inject any debug txns waiting in the debug queue { std::unique_lock trylock( @@ -1479,6 +1494,7 @@ TxQ::accept(Application& app, OpenView& view) } } + DBG_EXPORT("TxQ::accept SECTION 2: cron inject seq=" << view.info().seq); // Inject cron transactions, if any if (view.rules().enabled(featureCron)) { @@ -1541,49 +1557,49 @@ TxQ::accept(Application& app, OpenView& view) } } + DBG_EXPORT("TxQ::accept SECTION 3: export inject seq=" << view.info().seq); //@@start txq-inject-export // Inject exported transactions/signatures, if any if (view.rules().enabled(featureExport)) { do { + DBG_EXPORT("export inject: checking validator key"); // if we're not a validator we do nothing here if (app.getValidationPublicKey().empty()) + { + DBG_EXPORT("export inject: no validator key, skipping"); break; + } auto const& keys = app.getValidatorKeys(); if (keys.configInvalid()) + { + DBG_EXPORT("export inject: config invalid, skipping"); break; + } // and if we're not on the UNLReport we also do nothing - - auto const unlRep = view.read(keylet::UNLReport()); - if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators)) + // Use inUNLReport() which has a grace period for seq < 256 + // (testing) + if (!inUNLReport(view, app, keys.masterPublicKey, j_)) { - // nothing to do without a unlreport object + DBG_EXPORT("export inject: not in UNLReport, skipping"); break; } - bool found = false; - auto const& avs = unlRep->getFieldArray(sfActiveValidators); - for (auto const& av : avs) - { - if (PublicKey(av[sfPublicKey]) == keys.masterPublicKey) - { - found = true; - break; - } - } - - if (!found) - break; + DBG_EXPORT("export inject: we are a validator on UNLReport"); // execution to here means we're a validator and on the UNLReport Keylet const exportedDirKeylet{keylet::exportedDir()}; if (dirIsEmpty(view, exportedDirKeylet)) + { + DBG_EXPORT("export inject: exportedDir is empty, skipping"); break; + } + DBG_EXPORT("export inject: exportedDir has entries"); std::shared_ptr sleDirNode{}; unsigned int uDirEntry{0}; @@ -1637,18 +1653,28 @@ TxQ::accept(Application& app, OpenView& view) auto const& txnHash = sleItem->getFieldH256(sfTransactionHash); - auto exportedLgrSeq = exported.getFieldU32(sfLedgerSequence); + auto exportedLgrSeq = sleItem->getFieldU32(sfLedgerSequence); auto const seq = view.seq(); + DBG_EXPORT( + "export inject: entry exportedLgrSeq=" + << exportedLgrSeq << " viewSeq=" << seq); + if (exportedLgrSeq == seq) { + DBG_EXPORT( + "export inject: exportedLgrSeq == seq, skipping"); // this shouldn't happen, but do nothing continue; } if (exportedLgrSeq < seq - 1) { + DBG_EXPORT( + "export inject: condition met! " + << exportedLgrSeq << " < " << (seq - 1) + << ", creating ttEXPORT"); // all old entries need to be turned into Export // transactions so they can be removed from the directory @@ -1659,21 +1685,28 @@ TxQ::accept(Application& app, OpenView& view) // the ExportedTxn blob and publish the blob in the Export // transaction type. + DBG_EXPORT("export inject: getting signers array"); STArray signers = sleItem->getFieldArray(sfSigners); + DBG_EXPORT( + "export inject: signers count=" << signers.size()); auto s = std::make_shared(); exported.add(*s); SerialIter sitTrans(s->slice()); try { + DBG_EXPORT( + "export inject: creating STTx from exported txn"); auto stpTrans = std::make_shared(std::ref(sitTrans)); + DBG_EXPORT("export inject: STTx created"); if (!stpTrans->isFieldPresent(sfAccount) || stpTrans->getAccountID(sfAccount) == beast::zero) { - JLOG(j_.warn()) << "Hook: Export failure: " - << "sfAccount missing or zero."; + DBG_EXPORT( + "export inject: sfAccount missing or zero, " + "skipping"); // RH TODO: if this ever happens the entry should be // gracefully removed (somehow) continue; @@ -1685,30 +1718,42 @@ TxQ::accept(Application& app, OpenView& view) // Serialize the inner transaction and create an // STObject from it. sfExportedTxn is OBJECT type, not - // VL, so we must use emplace_back with STObject, not - // setFieldVL. + // VL. Use set() to replace the existing template field, + // not emplace_back which would add a duplicate. ripple::Serializer exportedSer; stpTrans->add(exportedSer); SerialIter exportedSit(exportedSer.slice()); + // Create ttEXPORT pseudo-transaction + // Note: sfSigners is already on the inner stpTrans + // (sfExportedTxn) ttEXPORT itself must NOT have + // sfSigners at top level (Change::preflight rejects it) STTx exportTx(ttEXPORT, [&](auto& obj) { - obj.emplace_back( - ripple::STObject(exportedSit, sfExportedTxn)); + // Pseudo-transaction required fields + // (Change::preflight checks) + obj[sfAccount] = AccountID(); + // Export-specific fields + obj.set(std::make_unique( + exportedSit, sfExportedTxn)); obj.setFieldU32(sfLedgerSequence, seq); obj.setFieldH256(sfTransactionHash, txnHash); - obj.setFieldArray(sfSigners, signers); }); - // submit to the ledger - { - uint256 txID = exportTx.getTransactionID(); - auto s = std::make_shared(); - exportTx.add(*s); - app.getHashRouter().setFlags(txID, SF_PRIVATE2); - app.getHashRouter().setFlags(txID, SF_EMITTED); - view.rawTxInsert(txID, std::move(s), nullptr); - ledgerChanged = true; - } + // Record the ttEXPORT transaction (like ttCRON) + // Cleanup happens via Change::applyExport() when + // processed + uint256 txID = exportTx.getTransactionID(); + DBG_EXPORT( + "export inject: rawTxInsert ttEXPORT txID=" + << txID); + + auto s = std::make_shared(); + exportTx.add(*s); + + app.getHashRouter().setFlags(txID, SF_PRIVATE2); + app.getHashRouter().setFlags(txID, SF_EMITTED); + view.rawTxInsert(txID, std::move(s), nullptr); + ledgerChanged = true; } catch (std::exception& e) @@ -1733,6 +1778,7 @@ TxQ::accept(Application& app, OpenView& view) } //@@end txq-inject-export + DBG_EXPORT("TxQ::accept SECTION 4: emit inject seq=" << view.info().seq); // Inject emitted transactions if any if (view.rules().enabled(featureHooks)) do @@ -1884,6 +1930,9 @@ TxQ::accept(Application& app, OpenView& view) } while (0); + DBG_EXPORT( + "TxQ::accept SECTION 5: process queue seq=" + << view.info().seq << " byFee_.size()=" << byFee_.size()); for (auto candidateIter = byFee_.begin(); candidateIter != byFee_.end();) { auto& account = byAccount_.at(candidateIter->account); @@ -2003,6 +2052,7 @@ TxQ::accept(Application& app, OpenView& view) } } + DBG_EXPORT("TxQ::accept SECTION 6: rebuild queue seq=" << view.info().seq); // All transactions that can be moved out of the queue into the open // ledger have been. Rebuild the queue using the open ledger's // parent hash, so that transactions paying the same fee are @@ -2010,6 +2060,18 @@ TxQ::accept(Application& app, OpenView& view) LedgerHash const& parentHash = view.info().parentHash; #if !NDEBUG auto const startingSize = byFee_.size(); + DBG_EXPORT( + "TxQ::accept CHECK seq=" << view.info().seq + << " parentHash=" << parentHash + << " parentHash_=" << parentHash_); + if (parentHash == parentHash_) + { + JLOG(j_.fatal()) << "TxQ::accept DOUBLE-ACCEPT DETECTED!" + << " seq=" << view.info().seq + << " parentHash=" << parentHash + << " parentHash_=" << parentHash_ + << " byFee_.size()=" << byFee_.size(); + } assert(parentHash != parentHash_); parentHash_ = parentHash; #endif diff --git a/src/ripple/app/tx/impl/ExportSign.cpp b/src/ripple/app/tx/impl/ExportSign.cpp index 69bc04cf3..e94025e6c 100644 --- a/src/ripple/app/tx/impl/ExportSign.cpp +++ b/src/ripple/app/tx/impl/ExportSign.cpp @@ -181,7 +181,7 @@ makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j) .getField(sfExportedTxn) .downcast(); - auto exportedLgrSeq = exported.getFieldU32(sfLedgerSequence); + auto exportedLgrSeq = sleItem->getFieldU32(sfLedgerSequence); // Only sign transactions that were added in the previous ledger if (exportedLgrSeq != seq - 1) diff --git a/src/ripple/ledger/impl/OpenView.cpp b/src/ripple/ledger/impl/OpenView.cpp index fd03d7665..2d5741ed6 100644 --- a/src/ripple/ledger/impl/OpenView.cpp +++ b/src/ripple/ledger/impl/OpenView.cpp @@ -19,6 +19,17 @@ #include #include +#include +#include + +// Debug macro for export investigation +#define DBG_DEREF(msg) \ + do \ + { \ + std::cerr << "[OpenView::dereference t=" << std::this_thread::get_id() \ + << "] " << msg << std::endl; \ + std::cerr.flush(); \ + } while (0) namespace ripple { @@ -59,16 +70,32 @@ public: value_type dereference() const override { + DBG_DEREF("ENTER key=" << iter_->first); value_type result; { + DBG_DEREF( + "creating SerialIter, txn size=" << iter_->second.txn->size()); SerialIter sit(iter_->second.txn->slice()); - result.first = std::make_shared(sit); + DBG_DEREF("creating STTx..."); + try + { + result.first = std::make_shared(sit); + DBG_DEREF("STTx created, type=" << result.first->getTxnType()); + } + catch (std::exception& e) + { + DBG_DEREF("STTx EXCEPTION: " << e.what()); + throw; + } } if (metadata_) { + DBG_DEREF("creating metadata"); SerialIter sit(iter_->second.meta->slice()); result.second = std::make_shared(sit, sfMetadata); + DBG_DEREF("metadata created"); } + DBG_DEREF("EXIT"); return result; } }; diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 9e2445fa1..2b8832ef9 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -388,6 +388,8 @@ LedgerFormats::LedgerFormats() {sfExportedTxn, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, + {sfTransactionHash, soeREQUIRED}, + {sfSigners, soeOPTIONAL}, }, commonFields); //@@end exported-txn-format diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index 9c7629701..6074153de 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace ripple { @@ -250,7 +251,17 @@ STObject::set(SerialIter& sit, int depth) }); if (dup != sf.cend()) + { + // Debug: dump all field names to identify the duplicate + std::cerr << "[STObject::set] DUPLICATE FIELD DETECTED!" << std::endl; + std::cerr << "[STObject::set] Duplicate field name: " + << (*dup)->getFName().getName() << std::endl; + std::cerr << "[STObject::set] All fields in object:" << std::endl; + for (auto const& f : sf) + std::cerr << " - " << f->getFName().getName() << std::endl; + std::cerr.flush(); Throw("Duplicate field detected"); + } return reachedEndOfObject; } diff --git a/src/ripple/rpc/handlers/LedgerAccept.cpp b/src/ripple/rpc/handlers/LedgerAccept.cpp index 3a01a3950..99778aa41 100644 --- a/src/ripple/rpc/handlers/LedgerAccept.cpp +++ b/src/ripple/rpc/handlers/LedgerAccept.cpp @@ -43,7 +43,7 @@ doLedgerAccept(RPC::JsonContext& context) else { std::unique_lock lock{context.app.getMasterMutex()}; - context.netOps.acceptLedger(); + context.netOps.acceptLedger(std::nullopt, "RPC:ledger_accept"); jvResult[jss::ledger_current_index] = context.ledgerMaster.getCurrentLedgerIndex(); } diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index 033710e95..9a1dd5149 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -477,6 +477,7 @@ struct Export_test : public beast::unit_test::suite *this, DebugLogs::Levels{ {"View", kTrace}, + {"TxQ", kTrace}, }); Env env{*this, makeConfig(), features, std::move(logs), kError}; @@ -520,16 +521,70 @@ struct Export_test : public beast::unit_test::suite // Close additional ledgers for signing flow env.close(); // N+1: validators submit ttEXPORT_SIGN - env.close(); // N+2: ttEXPORT removes entry + env.close(); // N+2: ttEXPORT created (rawTxInsert) + env.close(); // N+3: does ttEXPORT get applied here? // Check if cleanup happened { auto const exportedDirKey = keylet::exportedDir(); bool dirEmpty = dirIsEmpty(*env.current(), exportedDirKey); + + // Debug: if not empty, inspect what's in there + if (!dirEmpty && expectCleanup) + { + std::cerr << "=== DEBUG: exportedDir not empty ===" + << std::endl; + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + if (cdirFirst( + *env.current(), + exportedDirKey.key, + sleDirNode, + uDirEntry, + dirEntry)) + { + do + { + auto sleItem = + env.current()->read(Keylet{ltCHILD, dirEntry}); + if (sleItem) + { + std::cerr << " Entry: " << dirEntry << std::endl; + std::cerr << " LedgerSeq: " + << sleItem->getFieldU32(sfLedgerSequence) + << std::endl; + std::cerr + << " TxnHash: " + << sleItem->getFieldH256(sfTransactionHash) + << std::endl; + if (sleItem->isFieldPresent(sfSigners)) + { + auto const& signers = + sleItem->getFieldArray(sfSigners); + std::cerr + << " Signers count: " << signers.size() + << std::endl; + } + else + { + std::cerr << " Signers: NONE" << std::endl; + } + } + } while (cdirNext( + *env.current(), + exportedDirKey.key, + sleDirNode, + uDirEntry, + dirEntry)); + } + std::cerr << "=== END DEBUG ===" << std::endl; + } + BEAST_EXPECT(dirEmpty == expectCleanup); } - BEAST_EXPECT(env.current()->seq() == xportLedgerSeq + 3); + BEAST_EXPECT(env.current()->seq() == xportLedgerSeq + 4); } void @@ -564,11 +619,11 @@ struct Export_test : public beast::unit_test::suite using namespace test::jtx; FeatureBitset const all{supported_amendments()}; FeatureBitset const allWithExport{all | featureExport}; - testBasicSetup(all); - testEmitPayment(all); - testXportPayment(allWithExport); - // TODO: re-enable once validator signing flow is debugged - // testXportPaymentWithValidator(allWithExport); + // testBasicSetup(all); + // testEmitPayment(all); + // testXportPayment(allWithExport); + testXportPaymentWithValidator( + allWithExport); // with push_back disabled } }; diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 8a25ed3c7..8306d3ca6 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -130,7 +130,8 @@ Env::close( // Go through the rpc interface unless we need to simulate // a specific consensus delay. if (consensusDelay) - app().getOPs().acceptLedger(consensusDelay); + app().getOPs().acceptLedger( + consensusDelay, "Env::close(consensusDelay)"); else { auto resp = rpc("ledger_accept");