Files
rippled/src/xrpld/app/misc/NetworkOPs.cpp
Pratik Mankawde df01ba5d3d fix(ledger): stop the sweeper deleting acquires that are still being served
Two metric-level defects the sync analysis identified, fixed at the source rather
than worked around on the dashboard.

1. InboundLedgers::sweep() destroys any acquire idle for more than a minute, and
   that destruction is what telemetry reports as outcome=abandoned. But
   lastAction_ was only refreshed by the constructor, update() and done() --
   never by the receive path. With JtLedgerData capped at 3 concurrent jobs and
   33 acquires in flight, an acquire whose peers were answering normally could
   wait past the cutoff for its turn to apply data and be deleted for looking
   idle. Measured on a fresh mainnet sync: 490 abandoned acquires against ZERO
   expired retry budgets, so every one was a sweep, not a give-up.

   gotData() now calls touch(). The sweeper's idle test measures real inactivity
   instead of queue wait.

   lastAction_ had to become atomic to allow this. It was a plain
   clock_type::time_point written by the acquiring thread and read by sweep() on
   the timer thread; adding a third writer on peer threads would have been a data
   race. It is now std::atomic<clock_type::duration::rep> with relaxed ordering on
   both sides -- the sweeper compares against a 60-second threshold, so a value
   one tick stale cannot change its decision.

2. getLedgersBehindNetwork() returned the entire ledger sequence space on a fresh
   node. The existing floor only guarded being ahead of every peer; it did not
   guard having validated nothing at all, so validated=0 against a live tip gave
   105,892,534 -- an accurate subtraction of a meaningless quantity. It
   auto-scaled every consumer's axis and would trip any threshold. Distance to
   tip is undefined before the first validated ledger, so it now reports 0 until
   there is one, and the sync-state signals carry the initial-acquire progress.

   The clamp_max(1e6) added to the Ledgers Behind Network panel as a stopgap is
   removed: the metric is correct now, and leaving the clamp would hide a real
   large backlog.

Verified: clang-tidy over the full compile database reports no finding on any
changed line in the three files (the pre-existing misc-include-cleaner and
misc-const-correctness findings elsewhere in InboundLedger.cpp are untouched by
this change). pre-commit passes including clang-format and the Doxygen style
check; validate_dashboards passes.

Not verified: not compiled -- per instructions.md the build needs approval, so CI
is the first real compile of the atomic change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:30:08 +01:00

5069 lines
169 KiB
C++

#include <xrpl/server/NetworkOPs.h>
#include <xrpld/app/consensus/RCLConsensus.h>
#include <xrpld/app/consensus/RCLCxPeerPos.h>
#include <xrpld/app/consensus/RCLValidations.h>
#include <xrpld/app/ledger/AcceptedLedger.h>
#include <xrpld/app/ledger/InboundLedger.h>
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/LedgerToJson.h>
#include <xrpld/app/ledger/LocalTxs.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/main/LoadManager.h>
#include <xrpld/app/main/Tuning.h>
#include <xrpld/app/misc/DeliverMax.h>
#include <xrpld/app/misc/FeeVote.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/misc/ValidatorKeys.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/app/misc/make_NetworkOPs.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/core/Config.h>
#include <xrpld/overlay/Cluster.h>
#include <xrpld/overlay/ClusterNode.h>
#include <xrpld/overlay/Overlay.h>
#include <xrpld/overlay/predicates.h>
#include <xrpld/rpc/BookChanges.h>
#include <xrpld/rpc/CTID.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/telemetry/MetricMacros.h>
#include <xrpld/telemetry/MetricNames.h>
#include <xrpld/telemetry/MetricsRegistry.h>
#include <xrpld/telemetry/PropagationHelpers.h>
#include <xrpld/telemetry/TxSpanNames.h>
#include <xrpld/telemetry/TxTracing.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/ToString.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/UptimeClock.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/mulDiv.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/basics/scope.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/clock/abstract_clock.h>
#include <xrpl/beast/insight/Collector.h>
#include <xrpl/beast/insight/Gauge.h>
#include <xrpl/beast/insight/Hook.h>
#include <xrpl/beast/net/IPEndpoint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/beast/utility/rngfill.h>
#include <xrpl/config/Constants.h>
#include <xrpl/consensus/ConsensusParms.h>
#include <xrpl/consensus/ConsensusTypes.h>
#include <xrpl/core/ClosureCounter.h>
#include <xrpl/core/HashRouter.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/core/PerfLog.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/crypto/RFC1751.h>
#include <xrpl/crypto/csprng.h>
#include <xrpl/git/Git.h>
#include <xrpl/json/json_forwards.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/ledger/AcceptedLedgerTx.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/CanonicalTXSet.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/OrderBookDB.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/BuildInfo.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/protocol/NFTSyntheticSerializer.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/tokens.h>
#include <xrpl/rdb/RelationalDatabase.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/resource/Gossip.h>
#include <xrpl/resource/ResourceManager.h>
#include <xrpl/server/InfoSub.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/server/Manifest.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <xrpl/tx/apply.h>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/host_name.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
#include <boost/system/system_error.hpp>
#include <xrpl.pb.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <climits>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <functional>
#include <iterator>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
namespace xrpl {
class NetworkOPsImp final : public NetworkOPs
{
/**
* Transaction with input flags and results to be applied in batches.
*/
class TransactionStatus
{
public:
std::shared_ptr<Transaction> const transaction;
bool const admin;
bool const local;
FailHard const failType;
bool applied = false;
TER result;
std::shared_ptr<telemetry::SpanGuard> span; ///< Keeps the tx.process
///< span alive until the
///< batch processes this.
TransactionStatus(
std::shared_ptr<Transaction> t,
bool a,
bool l,
FailHard f,
std::shared_ptr<telemetry::SpanGuard> s = nullptr)
: transaction(std::move(t)), admin(a), local(l), failType(f), span(std::move(s))
{
XRPL_ASSERT(
local || failType == FailHard::No,
"xrpl::NetworkOPsImp::TransactionStatus::TransactionStatus : "
"valid inputs");
}
};
/**
* Synchronization states for transaction batches.
*/
enum class DispatchState : unsigned char {
None,
Scheduled,
Running,
};
static std::array<char const*, 5> const kStates;
/**
* State accounting records two attributes for each possible server state:
* 1) Amount of time spent in each state (in microseconds). This value is
* updated upon each state transition.
* 2) Number of transitions to each state.
*
* This data can be polled through server_info and represented by
* monitoring systems similarly to how bandwidth, CPU, and other
* counter-based metrics are managed.
*
* State accounting is more accurate than periodic sampling of server
* state. With periodic sampling, it is very likely that state transitions
* are missed, and accuracy of time spent in each state is very rough.
*/
class StateAccounting
{
struct Counters
{
explicit Counters() = default;
std::uint64_t transitions = 0;
std::chrono::microseconds dur = std::chrono::microseconds(0);
};
OperatingMode mode_ = OperatingMode::DISCONNECTED;
std::array<Counters, 5> counters_;
mutable std::mutex mutex_;
std::chrono::steady_clock::time_point start_ = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point const processStart_ = start_;
std::uint64_t initialSyncUs_{0};
static std::array<json::StaticString const, 5> const kStates;
public:
explicit StateAccounting()
{
counters_[static_cast<std::size_t>(OperatingMode::DISCONNECTED)].transitions = 1;
}
/**
* Record state transition. Update duration spent in previous
* state.
*
* @param om New state.
*/
void
mode(OperatingMode om);
/**
* Output state counters in JSON format.
*
* @obj Json object to which to add state accounting data.
*/
void
json(json::Value& obj) const;
struct CounterData
{
decltype(counters_) counters;
decltype(mode_) mode = OperatingMode::DISCONNECTED;
decltype(start_) start;
decltype(initialSyncUs_) initialSyncUs{};
};
CounterData
getCounterData() const
{
std::scoped_lock const lock(mutex_);
return {
.counters = counters_,
.mode = mode_,
.start = start_,
.initialSyncUs = initialSyncUs_};
}
/**
* Time spent in the current operating mode so far. This is the same
* quantity reported as `server_state_duration_us` in json(): the
* elapsed time since the last state transition. Thread-safe.
*
* @return Duration since entering the current state, in microseconds.
*/
std::chrono::microseconds
currentStateDurationUs() const
{
std::scoped_lock const lock(mutex_);
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start_);
}
/**
* Microseconds from process start to the first FULL transition. This
* is the same quantity reported as `initial_sync_duration_us` in
* json(); reading it alone avoids copying the whole counter array.
* Thread-safe.
*
* @return Time to first FULL, or 0 if FULL was never reached.
*/
std::uint64_t
initialSyncDurationUs() const
{
std::scoped_lock const lock(mutex_);
return initialSyncUs_;
}
};
/**
* Server fees published on `server` subscription
*/
struct ServerFeeSummary
{
ServerFeeSummary() = default;
ServerFeeSummary(
XRPAmount fee,
TxQ::Metrics escalationMetrics, // trivially copyable
LoadFeeTrack const& loadFeeTrack);
bool
operator!=(ServerFeeSummary const& b) const;
bool
operator==(ServerFeeSummary const& b) const
{
return !(*this != b);
}
std::uint32_t loadFactorServer = 256;
std::uint32_t loadBaseServer = 256;
XRPAmount baseFee{10};
std::optional<TxQ::Metrics> em = std::nullopt;
};
public:
NetworkOPsImp(
ServiceRegistry& registry,
NetworkOPs::clock_type& clock,
bool standalone,
std::size_t minPeerCount,
bool startValid,
JobQueue& jobQueue,
LedgerMaster& ledgerMaster,
ValidatorKeys const& validatorKeys,
boost::asio::io_context& ioCtx,
beast::Journal journal,
beast::insight::Collector::ptr const& collector)
: registry_(registry)
, journal_(journal)
, localTX_(makeLocalTxs())
, mode_(startValid ? OperatingMode::FULL : OperatingMode::DISCONNECTED)
, heartbeatTimer_(ioCtx)
, clusterTimer_(ioCtx)
, accountHistoryTxTimer_(ioCtx)
, consensus_(
registry_.get().getApp(),
makeFeeVote(
setupFeeVote(registry_.get().getApp().config().section(Sections::kVoting)),
registry_.get().getJournal("FeeVote")),
ledgerMaster,
*localTX_,
registry.getInboundTransactions(),
beast::getAbstractClock<std::chrono::steady_clock>(),
validatorKeys,
registry_.get().getJournal("LedgerConsensus"))
, validatorPK_(
validatorKeys.keys ? validatorKeys.keys->publicKey : decltype(validatorPK_){})
, validatorMasterPK_(
validatorKeys.keys ? validatorKeys.keys->masterPublicKey
: decltype(validatorMasterPK_){})
, ledgerMaster_(ledgerMaster)
, jobQueue_(jobQueue)
, standalone_(standalone)
, minPeerCount_(startValid ? 0 : minPeerCount)
, stats_([this] { collectMetrics(); }, collector)
{
}
~NetworkOPsImp() override
{
// This clear() is necessary to ensure the shared_ptrs in this map get
// destroyed NOW because the objects in this map invoke methods on this
// class when they are destroyed
rpcSubMap_.clear();
}
public:
OperatingMode
getOperatingMode() const override;
std::chrono::microseconds
getServerStateDurationUs() const override;
std::uint64_t
getInitialSyncDurationUs() const override;
std::string
strOperatingMode(OperatingMode const mode, bool const admin) const override;
std::string
strOperatingMode(bool const admin = false) const override;
std::uint32_t
getLedgersBehindNetwork() const override;
//
// Transaction operations.
//
// Must complete immediately.
void
submitTransaction(std::shared_ptr<STTx const> const&) override;
void
processTransaction(
std::shared_ptr<Transaction>& transaction,
bool bUnlimited,
bool bLocal,
FailHard failType) override;
void
processTransactionSet(CanonicalTXSet const& set) override;
/**
* For transactions submitted directly by a client, apply batch of
* transactions and wait for this transaction to complete.
*
* @param transaction Transaction object.
* @param bUnlimited Whether a privileged client connection submitted it.
* @param failType fail_hard setting from transaction submission.
* @param span Optional tx.process span to keep alive across the
* batch boundary so its context propagates to peers.
*/
void
doTransactionSync(
std::shared_ptr<Transaction> transaction,
bool bUnlimited,
FailHard failType,
std::shared_ptr<telemetry::SpanGuard> span = nullptr);
/**
* For transactions not submitted by a locally connected client, fire and
* forget. Add to batch and trigger it to be processed if there's no batch
* currently being applied.
*
* @param transaction Transaction object
* @param bUnlimited Whether a privileged client connection submitted it.
* @param failType fail_hard setting from transaction submission.
*/
void
doTransactionAsync(
std::shared_ptr<Transaction> transaction,
bool bUnlimited,
FailHard failtype,
std::shared_ptr<telemetry::SpanGuard> span = nullptr);
private:
bool
preProcessTransaction(std::shared_ptr<Transaction>& transaction);
void
doTransactionSyncBatch(
std::unique_lock<std::mutex>& lock,
std::function<bool(std::unique_lock<std::mutex> const&)> retryCallback);
public:
/**
* Apply transactions in batches. Continue until none are queued.
*/
void
transactionBatch();
/**
* Attempt to apply transactions and post-process based on the results.
*
* @param Lock that protects the transaction batching
*/
void
apply(std::unique_lock<std::mutex>& batchLock);
//
// Owner functions.
//
json::Value
getOwnerInfo(std::shared_ptr<ReadView const> lpLedger, AccountID const& account) override;
//
// Book functions.
//
void
getBookPage(
std::shared_ptr<ReadView const>& lpLedger,
Book const&,
AccountID const& uTakerID,
bool const bProof,
unsigned int iLimit,
json::Value const& jvMarker,
json::Value& jvResult) override;
// Ledger proposal/close functions.
bool
processTrustedProposal(RCLCxPeerPos proposal) override;
bool
recvValidation(std::shared_ptr<STValidation> const& val, std::string const& source) override;
void
mapComplete(std::shared_ptr<SHAMap> const& map, bool fromAcquire) override;
// Network state machine.
// Used for the "jump" case.
private:
void
switchLastClosedLedger(std::shared_ptr<Ledger const> const& newLCL);
bool
checkLastClosedLedger(Overlay::PeerSequence const&, uint256& networkClosed);
public:
bool
beginConsensus(uint256 const& networkClosed, std::unique_ptr<std::stringstream> const& clog)
override;
void
endConsensus(std::unique_ptr<std::stringstream> const& clog) override;
void
setStandAlone() override;
/**
* Called to initially start our timers.
* Not called for stand-alone mode.
*/
void
setStateTimer() override;
void
setNeedNetworkLedger() override;
void
clearNeedNetworkLedger() override;
bool
isNeedNetworkLedger() override;
bool
isFull() override;
void
setMode(OperatingMode om) override;
bool
isBlocked() override;
bool
isAmendmentBlocked() override;
void
setAmendmentBlocked() override;
bool
isAmendmentWarned() override;
void
setAmendmentWarned() override;
void
clearAmendmentWarned() override;
bool
isUNLBlocked() override;
void
setUNLBlocked() override;
void
clearUNLBlocked() override;
void
consensusViewChange() override;
json::Value
getConsensusInfo() override;
json::Value
getServerInfo(bool human, bool admin, bool counters) override;
void
clearLedgerFetch() override;
json::Value
getLedgerFetchInfo() override;
std::uint32_t
acceptLedger(std::optional<std::chrono::milliseconds> consensusDelay) override;
void
reportFeeChange() override;
void
reportConsensusStateChange(ConsensusPhase phase);
void
updateLocalTx(ReadView const& view) override;
std::size_t
getLocalTxCount() override;
std::size_t
getBookSubscribersCount() override;
//
// Monitoring: publisher side.
//
void
pubLedger(std::shared_ptr<ReadView const> const& lpAccepted) override;
void
pubProposedTransaction(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const& transaction,
TER result) override;
void
pubValidation(std::shared_ptr<STValidation> const& val) override;
//--------------------------------------------------------------------------
//
// InfoSub::Source.
//
void
subAccount(InfoSub::ref ispListener, hash_set<AccountID> const& vnaAccountIDs, bool rt)
override;
void
unsubAccount(InfoSub::ref ispListener, hash_set<AccountID> const& vnaAccountIDs, bool rt)
override;
// Just remove the subscription from the tracking
// not from the InfoSub. Needed for InfoSub destruction
void
unsubAccountInternal(std::uint64_t seq, hash_set<AccountID> const& vnaAccountIDs, bool rt)
override;
ErrorCodeI
subAccountHistory(InfoSub::ref ispListener, AccountID const& account) override;
void
unsubAccountHistory(InfoSub::ref ispListener, AccountID const& account, bool historyOnly)
override;
void
unsubAccountHistoryInternal(std::uint64_t seq, AccountID const& account, bool historyOnly)
override;
bool
subLedger(InfoSub::ref ispListener, json::Value& jvResult) override;
bool
unsubLedger(std::uint64_t uListener) override;
bool
subBookChanges(InfoSub::ref ispListener) override;
bool
unsubBookChanges(std::uint64_t uListener) override;
bool
subServer(InfoSub::ref ispListener, json::Value& jvResult, bool admin) override;
bool
unsubServer(std::uint64_t uListener) override;
bool
subBook(InfoSub::ref ispListener, Book const&) override;
bool
unsubBook(InfoSub::ref ispListener, Book const&) override;
bool
unsubBookInternal(std::uint64_t uListener, Book const&) override;
bool
subManifests(InfoSub::ref ispListener) override;
bool
unsubManifests(std::uint64_t uListener) override;
void
pubManifest(Manifest const&) override;
bool
subTransactions(InfoSub::ref ispListener) override;
bool
unsubTransactions(std::uint64_t uListener) override;
bool
subRTTransactions(InfoSub::ref ispListener) override;
bool
unsubRTTransactions(std::uint64_t uListener) override;
bool
subValidations(InfoSub::ref ispListener) override;
bool
unsubValidations(std::uint64_t uListener) override;
bool
subPeerStatus(InfoSub::ref ispListener) override;
bool
unsubPeerStatus(std::uint64_t uListener) override;
void
pubPeerStatus(std::function<json::Value(void)> const&) override;
bool
subConsensus(InfoSub::ref ispListener) override;
bool
unsubConsensus(std::uint64_t uListener) override;
InfoSub::pointer
findRpcSub(std::string const& strUrl) override;
InfoSub::pointer
addRpcSub(std::string const& strUrl, InfoSub::ref) override;
bool
tryRemoveRpcSub(std::string const& strUrl) override;
beast::Journal const&
journal() const override
{
return journal_;
}
void
stop() override
{
{
try
{
heartbeatTimer_.cancel();
}
catch (boost::system::system_error const& e)
{
JLOG(journal_.error()) << "NetworkOPs: heartbeatTimer cancel error: " << e.what();
}
try
{
clusterTimer_.cancel();
}
catch (boost::system::system_error const& e)
{
JLOG(journal_.error()) << "NetworkOPs: clusterTimer cancel error: " << e.what();
}
try
{
accountHistoryTxTimer_.cancel();
}
catch (boost::system::system_error const& e)
{
JLOG(journal_.error())
<< "NetworkOPs: accountHistoryTxTimer cancel error: " << e.what();
}
}
// Make sure that any waitHandlers pending in our timers are done.
using namespace std::chrono_literals;
waitHandlerCounter_.join("NetworkOPs", 1s, journal_);
}
void
stateAccounting(json::Value& obj) override;
private:
void
setTimer(
boost::asio::steady_timer& timer,
std::chrono::milliseconds const& expiryTime,
std::function<void()> onExpire,
std::function<void()> onError);
void
setHeartbeatTimer();
void
setClusterTimer();
void
processHeartbeatTimer();
void
processClusterTimer();
MultiApiJson
transJson(
std::shared_ptr<STTx const> const& transaction,
TER result,
bool validated,
std::shared_ptr<ReadView const> const& ledger,
std::optional<std::reference_wrapper<TxMeta const>> meta);
void
pubValidatedTransaction(
std::shared_ptr<ReadView const> const& ledger,
AcceptedLedgerTx const& transaction,
bool last);
void
pubAccountTransaction(
std::shared_ptr<ReadView const> const& ledger,
AcceptedLedgerTx const& transaction,
bool last);
/**
* Fan transaction notifications out to all book subscribers.
*
* Extracts the set of order books affected by @p transaction, then
* delivers @p jvObj to every live subscriber of those books.
*
* Uses a two-pass design to keep subLock_ hold time short:
* 1. Under subLock_, collect strong InfoSub pointers for all live
* subscribers and prune any expired weak_ptrs encountered.
* 2. Release subLock_, then call send() on each collected pointer.
*
* @param transaction The accepted ledger transaction to inspect.
* @param jvObj JSON representation of the transaction to deliver.
*
* @note Thread-safety: acquires subLock_ for the collection pass only.
* send() is intentionally called outside the lock to avoid blocking
* all other sub/unsub/publish paths while I/O is in progress.
* @note Contention: subLock_ is shared with all other subscription types.
* On high-throughput nodes processing multi-hop payments that touch
* many offer nodes, this pass holds subLock_ longer than the old
* per-book BookListeners locks did. This is an accepted trade-off
* for lock-domain simplicity.
*/
void
pubBookTransaction(AcceptedLedgerTx const& transaction, MultiApiJson const& jvObj);
void
pubProposedAccountTransaction(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const& transaction,
TER result);
void
pubServer();
void
pubConsensus(ConsensusPhase phase);
std::string
getHostId(bool forAdmin);
private:
using SubMapType = hash_map<std::uint64_t, InfoSub::wptr>;
using SubInfoMapType = hash_map<AccountID, SubMapType>;
using subRpcMapType = hash_map<std::string, InfoSub::pointer>;
/*
* With a validated ledger to separate history and future, the node
* streams historical txns with negative indexes starting from -1,
* and streams future txns starting from index 0.
* The SubAccountHistoryIndex struct maintains these indexes.
* It also has a flag stopHistorical_ for stopping streaming
* the historical txns.
*/
struct SubAccountHistoryIndex
{
AccountID const accountId;
// forward
std::uint32_t forwardTxIndex{0};
// separate backward and forward
std::uint32_t separationLedgerSeq{0};
// history, backward
std::uint32_t historyLastLedgerSeq{0};
std::int32_t historyTxIndex{-1};
bool haveHistorical{false};
std::atomic<bool> stopHistorical{false};
SubAccountHistoryIndex(AccountID const& accountId) : accountId(accountId)
{
}
};
struct SubAccountHistoryInfo
{
InfoSub::pointer sink;
std::shared_ptr<SubAccountHistoryIndex> index;
};
struct SubAccountHistoryInfoWeak
{
InfoSub::wptr sinkWptr;
std::shared_ptr<SubAccountHistoryIndex> index;
};
using SubAccountHistoryMapType =
hash_map<AccountID, hash_map<std::uint64_t, SubAccountHistoryInfoWeak>>;
/**
* @note called while holding subLock_
*/
void
subAccountHistoryStart(
std::shared_ptr<ReadView const> const& ledger,
SubAccountHistoryInfoWeak& subInfo);
void
addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo);
void
setAccountHistoryJobTimer(SubAccountHistoryInfoWeak subInfo);
std::reference_wrapper<ServiceRegistry> registry_;
beast::Journal journal_;
std::unique_ptr<LocalTxs> localTX_;
std::recursive_mutex subLock_;
std::atomic<OperatingMode> mode_;
std::atomic<bool> needNetworkLedger_{false};
std::atomic<bool> amendmentBlocked_{false};
std::atomic<bool> amendmentWarned_{false};
std::atomic<bool> unlBlocked_{false};
ClosureCounter<void, boost::system::error_code const&> waitHandlerCounter_;
boost::asio::steady_timer heartbeatTimer_;
boost::asio::steady_timer clusterTimer_;
boost::asio::steady_timer accountHistoryTxTimer_;
RCLConsensus consensus_;
std::optional<PublicKey> const validatorPK_;
std::optional<PublicKey> const validatorMasterPK_;
ConsensusPhase lastConsensusPhase_{ConsensusPhase::Open};
LedgerMaster& ledgerMaster_;
/**
* Maps each order book to its current set of subscribers.
* Outer key: the Book (currency pair + optional domain).
* Inner key: InfoSub::seq (unique per connection).
* Inner value: weak_ptr so that a dropped connection does not prevent
* the InfoSub from being destroyed; expired entries are pruned lazily
* by pubBookTransaction and eagerly by unsubBookInternal (~InfoSub path).
* Guarded by subLock_.
*/
using SubBookMapType = hash_map<Book, SubMapType>;
SubInfoMapType subAccount_;
SubInfoMapType subRTAccount_;
SubBookMapType subBook_; ///< Guarded by subLock_.
subRpcMapType rpcSubMap_;
SubAccountHistoryMapType subAccountHistory_;
// Used as array indices; converting to enum class would require casts at ~40 call sites.
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum SubTypes {
SLedger, // Accepted ledgers.
SManifests, // Received validator manifests.
SServer, // When server changes connectivity state.
STransactions, // All accepted transactions.
SRtTransactions, // All proposed and accepted transactions.
SValidations, // Received validations.
SPeerStatus, // Peer status changes.
SConsensusPhase, // Consensus phase
SBookChanges, // Per-ledger order book changes
SLastEntry // Any new entry must be ADDED ABOVE this one
};
std::array<SubMapType, SubTypes::SLastEntry> streamMaps_;
ServerFeeSummary lastFeeSummary_;
JobQueue& jobQueue_;
// Whether we are in standalone mode.
bool const standalone_;
// The number of nodes that we need to consider ourselves connected.
std::size_t const minPeerCount_;
// Transaction batching.
std::condition_variable cond_;
std::mutex mutex_;
DispatchState dispatchState_ = DispatchState::None;
std::vector<TransactionStatus> transactions_;
StateAccounting accounting_;
std::set<uint256> pendingValidations_;
std::mutex validationsMutex_;
private:
struct Stats
{
template <class Handler>
Stats(Handler const& handler, beast::insight::Collector::ptr const& collector)
: hook(collector->makeHook(handler))
, disconnectedDuration(
collector->makeGauge("State_Accounting", "Disconnected_duration"))
, connectedDuration(collector->makeGauge("State_Accounting", "Connected_duration"))
, syncingDuration(collector->makeGauge("State_Accounting", "Syncing_duration"))
, trackingDuration(collector->makeGauge("State_Accounting", "Tracking_duration"))
, fullDuration(collector->makeGauge("State_Accounting", "Full_duration"))
, disconnectedTransitions(
collector->makeGauge("State_Accounting", "Disconnected_transitions"))
, connectedTransitions(
collector->makeGauge("State_Accounting", "Connected_transitions"))
, syncingTransitions(collector->makeGauge("State_Accounting", "Syncing_transitions"))
, trackingTransitions(collector->makeGauge("State_Accounting", "Tracking_transitions"))
, fullTransitions(collector->makeGauge("State_Accounting", "Full_transitions"))
{
}
beast::insight::Hook hook;
beast::insight::Gauge disconnectedDuration;
beast::insight::Gauge connectedDuration;
beast::insight::Gauge syncingDuration;
beast::insight::Gauge trackingDuration;
beast::insight::Gauge fullDuration;
beast::insight::Gauge disconnectedTransitions;
beast::insight::Gauge connectedTransitions;
beast::insight::Gauge syncingTransitions;
beast::insight::Gauge trackingTransitions;
beast::insight::Gauge fullTransitions;
};
std::mutex statsMutex_; // Mutex to lock stats_
Stats stats_;
private:
void
collectMetrics();
};
//------------------------------------------------------------------------------
static std::array<char const*, 5> const kStateNames{
{"disconnected", "connected", "syncing", "tracking", "full"}};
std::array<char const*, 5> const NetworkOPsImp::kStates = kStateNames;
std::array<json::StaticString const, 5> const NetworkOPsImp::StateAccounting::kStates = {
{json::StaticString(kStateNames[0]),
json::StaticString(kStateNames[1]),
json::StaticString(kStateNames[2]),
json::StaticString(kStateNames[3]),
json::StaticString(kStateNames[4])}};
static auto const kGenesisAccountId =
calcAccountID(generateKeyPair(KeyType::Secp256k1, generateSeed("masterpassphrase")).first);
//------------------------------------------------------------------------------
inline OperatingMode
NetworkOPsImp::getOperatingMode() const
{
return mode_;
}
std::chrono::microseconds
NetworkOPsImp::getServerStateDurationUs() const
{
return accounting_.currentStateDurationUs();
}
std::uint64_t
NetworkOPsImp::getInitialSyncDurationUs() const
{
return accounting_.initialSyncDurationUs();
}
std::uint32_t
NetworkOPsImp::getLedgersBehindNetwork() const
{
// The network tip is the highest ledger sequence any connected peer says it
// holds. Peers report their range in mtSTATUS_CHANGE, which PeerImp caches,
// so this is a read of already-received data: no new network round trip.
std::uint32_t networkTarget = 0;
registry_.get().getOverlay().foreach([&networkTarget](std::shared_ptr<Peer> const& peer) {
std::uint32_t minSeq = 0;
std::uint32_t maxSeq = 0;
peer->ledgerRange(minSeq, maxSeq);
networkTarget = std::max(networkTarget, maxSeq);
});
auto const validated = registry_.get().getLedgerMaster().getValidLedgerIndex();
// A node that has validated nothing is not "behind" by the whole sequence
// space; the distance is undefined until there is a validated ledger to
// measure from. Returning the raw difference here reported ~105.9 million on
// a fresh mainnet start, which is a real reading of a meaningless quantity:
// it auto-scaled every consumer's axis and would trip any threshold. Report
// zero until the first ledger is validated, and let the sync-state signals
// say how far along the initial acquire is.
if (validated == 0)
return 0;
// Floor at zero: we can legitimately be ahead of every peer's reported
// range, and a peer that has reported nothing yet leaves the target at 0.
return networkTarget > validated ? networkTarget - validated : 0;
}
inline std::string
NetworkOPsImp::strOperatingMode(bool const admin /* = false */) const
{
return strOperatingMode(mode_, admin);
}
inline void
NetworkOPsImp::setStandAlone()
{
setMode(OperatingMode::FULL);
}
inline void
NetworkOPsImp::setNeedNetworkLedger()
{
needNetworkLedger_ = true;
}
inline void
NetworkOPsImp::clearNeedNetworkLedger()
{
needNetworkLedger_ = false;
}
inline bool
NetworkOPsImp::isNeedNetworkLedger()
{
return needNetworkLedger_;
}
inline bool
NetworkOPsImp::isFull()
{
return !needNetworkLedger_ && (mode_ == OperatingMode::FULL);
}
std::string
NetworkOPsImp::getHostId(bool forAdmin)
{
static std::string const kHostname = boost::asio::ip::host_name();
if (forAdmin)
return kHostname;
// For non-admin uses hash the node public key into a
// single RFC1751 word:
static std::string const kShroudedHostId = [this]() {
auto const& id = registry_.get().getApp().nodeIdentity();
return RFC1751::getWordFromBlob(id.first.data(), id.first.size());
}();
return kShroudedHostId;
}
void
NetworkOPsImp::setStateTimer()
{
setHeartbeatTimer();
// Only do this work if a cluster is configured
if (registry_.get().getCluster().size() != 0)
setClusterTimer();
}
void
NetworkOPsImp::setTimer(
boost::asio::steady_timer& timer,
std::chrono::milliseconds const& expiryTime,
std::function<void()> onExpire,
std::function<void()> onError)
{
// Only start the timer if waitHandlerCounter_ is not yet joined.
if (auto optionalCountedHandler =
waitHandlerCounter_.wrap([this, onExpire, onError](boost::system::error_code const& e) {
if ((e.value() == boost::system::errc::success) && (!jobQueue_.isStopped()))
{
onExpire();
}
// Recover as best we can if an unexpected error occurs.
if (e.value() != boost::system::errc::success &&
e.value() != boost::asio::error::operation_aborted)
{
// Try again later and hope for the best.
JLOG(journal_.error())
<< "Timer got error '" << e.message() << "'. Restarting timer.";
onError();
}
}))
{
timer.expires_after(expiryTime);
timer.async_wait(std::move(*optionalCountedHandler));
}
}
void
NetworkOPsImp::setHeartbeatTimer()
{
setTimer(
heartbeatTimer_,
consensus_.parms().ledgerGRANULARITY,
[this]() {
jobQueue_.addJob(JtNetopTimer, "NetHeart", [this]() { processHeartbeatTimer(); });
},
[this]() { setHeartbeatTimer(); });
}
void
NetworkOPsImp::setClusterTimer()
{
using namespace std::chrono_literals;
setTimer(
clusterTimer_,
10s,
[this]() {
jobQueue_.addJob(JtNetopCluster, "NetCluster", [this]() { processClusterTimer(); });
},
[this]() { setClusterTimer(); });
}
void
NetworkOPsImp::setAccountHistoryJobTimer(SubAccountHistoryInfoWeak subInfo)
{
JLOG(journal_.debug()) << "Scheduling AccountHistory job for account "
<< toBase58(subInfo.index->accountId);
using namespace std::chrono_literals;
setTimer(
accountHistoryTxTimer_,
4s,
[this, subInfo]() { addAccountHistoryJob(subInfo); },
[this, subInfo]() { setAccountHistoryJobTimer(subInfo); });
}
void
NetworkOPsImp::processHeartbeatTimer()
{
RclConsensusLogger clog("Heartbeat Timer", consensus_.validating(), journal_);
{
std::unique_lock lock{registry_.get().getApp().getMasterMutex()};
// VFALCO NOTE This is for diagnosing a crash on exit
LoadManager& mgr(registry_.get().getLoadManager());
mgr.heartbeat();
std::size_t const numPeers = registry_.get().getOverlay().size();
// do we have sufficient peers? If not, we are disconnected.
if (numPeers < minPeerCount_)
{
if (mode_ != OperatingMode::DISCONNECTED)
{
setMode(OperatingMode::DISCONNECTED);
std::stringstream ss;
ss << "Node count (" << numPeers << ") has fallen "
<< "below required minimum (" << minPeerCount_ << ").";
JLOG(journal_.warn()) << ss.str();
CLOG(clog.ss()) << "set mode to DISCONNECTED: " << ss.str();
}
else
{
CLOG(clog.ss()) << "already DISCONNECTED. too few peers (" << numPeers
<< "), need at least " << minPeerCount_;
}
// MasterMutex lock need not be held to call setHeartbeatTimer()
lock.unlock();
// We do not call consensus_.timerEntry until there are enough
// peers providing meaningful inputs to consensus
setHeartbeatTimer();
return;
}
if (mode_ == OperatingMode::DISCONNECTED)
{
setMode(OperatingMode::CONNECTED);
JLOG(journal_.info()) << "Node count (" << numPeers << ") is sufficient.";
CLOG(clog.ss()) << "setting mode to CONNECTED based on " << numPeers << " peers. ";
}
// Check if the last validated ledger forces a change between these
// states.
auto origMode = mode_.load();
CLOG(clog.ss()) << "mode: " << strOperatingMode(origMode, true);
if (mode_ == OperatingMode::SYNCING)
{
setMode(OperatingMode::SYNCING);
}
else if (mode_ == OperatingMode::CONNECTED)
{
setMode(OperatingMode::CONNECTED);
}
auto newMode = mode_.load();
if (origMode != newMode)
{
CLOG(clog.ss()) << ", changing to " << strOperatingMode(newMode, true);
}
CLOG(clog.ss()) << ". ";
}
consensus_.timerEntry(registry_.get().getTimeKeeper().closeTime(), clog.ss());
CLOG(clog.ss()) << "consensus phase " << to_string(lastConsensusPhase_);
ConsensusPhase const currPhase = consensus_.phase();
if (lastConsensusPhase_ != currPhase)
{
reportConsensusStateChange(currPhase);
lastConsensusPhase_ = currPhase;
CLOG(clog.ss()) << " changed to " << to_string(lastConsensusPhase_);
}
CLOG(clog.ss()) << ". ";
setHeartbeatTimer();
}
void
NetworkOPsImp::processClusterTimer()
{
if (registry_.get().getCluster().size() == 0)
return;
using namespace std::chrono_literals;
bool const update = registry_.get().getCluster().update(
registry_.get().getApp().nodeIdentity().first,
"",
(ledgerMaster_.getValidatedLedgerAge() <= 4min)
? registry_.get().getFeeTrack().getLocalFee()
: 0,
registry_.get().getTimeKeeper().now());
if (!update)
{
JLOG(journal_.debug()) << "Too soon to send cluster update";
setClusterTimer();
return;
}
protocol::TMCluster cluster;
registry_.get().getCluster().forEach([&cluster](ClusterNode const& node) {
protocol::TMClusterNode& n = *cluster.add_clusternodes();
n.set_publickey(toBase58(TokenType::NodePublic, node.identity()));
n.set_reporttime(node.getReportTime().time_since_epoch().count());
n.set_nodeload(node.getLoadFee());
if (!node.name().empty())
n.set_nodename(node.name());
});
Resource::Gossip const gossip = registry_.get().getResourceManager().exportConsumers();
for (auto& item : gossip.items)
{
protocol::TMLoadSource& node = *cluster.add_loadsources();
node.set_name(to_string(item.address));
node.set_cost(item.balance);
}
registry_.get().getOverlay().foreach(
sendIf(std::make_shared<Message>(cluster, protocol::mtCLUSTER), PeerInCluster()));
setClusterTimer();
}
//------------------------------------------------------------------------------
std::string
NetworkOPsImp::strOperatingMode(OperatingMode const mode, bool const admin) const
{
if (mode == OperatingMode::FULL && admin)
{
auto const consensusMode = consensus_.mode();
if (consensusMode != ConsensusMode::WrongLedger)
{
if (consensusMode == ConsensusMode::Proposing)
return "proposing";
if (consensus_.validating())
return "validating";
}
}
return kStates[static_cast<std::size_t>(mode)];
}
void
NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
{
if (isNeedNetworkLedger())
{
// Nothing we can do if we've never been in sync
return;
}
// Reject any transaction with the tfInnerBatchTxn flag at the network
// boundary, regardless of amendment state.
if (iTrans->isFlag(tfInnerBatchTxn))
{
JLOG(journal_.error()) << "Submitted transaction invalid: tfInnerBatchTxn flag present.";
return;
}
// this is an asynchronous interface
auto const trans = sterilize(*iTrans);
auto const txid = trans->getTransactionID();
auto const flags = registry_.get().getHashRouter().getFlags(txid);
if ((flags & HashRouterFlags::BAD) != HashRouterFlags::UNDEFINED)
{
JLOG(journal_.warn()) << "Submitted transaction cached bad";
return;
}
try
{
auto const [validity, reason] = checkValidity(
registry_.get().getHashRouter(), *trans, ledgerMaster_.getValidatedRules());
if (validity != Validity::Valid)
{
JLOG(journal_.warn()) << "Submitted transaction invalid: " << reason;
return;
}
}
catch (std::exception const& ex)
{
JLOG(journal_.warn()) << "Exception checking transaction " << txid << ": " << ex.what();
return;
}
std::string reason;
auto tx = std::make_shared<Transaction>(trans, reason, registry_.get().getApp());
jobQueue_.addJob(JtTransaction, "SubmitTxn", [this, tx]() {
auto t = tx;
processTransaction(t, false, false, FailHard::No);
});
}
bool
NetworkOPsImp::preProcessTransaction(std::shared_ptr<Transaction>& transaction)
{
auto const newFlags = registry_.get().getHashRouter().getFlags(transaction->getID());
if ((newFlags & HashRouterFlags::BAD) != HashRouterFlags::UNDEFINED)
{
// cached bad
JLOG(journal_.warn()) << transaction->getID() << ": cached bad!\n";
transaction->setStatus(TransStatus::INVALID);
transaction->setResult(temBAD_SIGNATURE);
return false;
}
auto const view = ledgerMaster_.getCurrentLedger();
// This function is called by several different parts of the codebase
// under no circumstances will we ever accept an inner txn within a batch
// txn from the network.
auto const sttx = *transaction->getSTransaction();
if (sttx.isFlag(tfInnerBatchTxn))
{
transaction->setStatus(TransStatus::INVALID);
transaction->setResult(temINVALID_FLAG);
registry_.get().getHashRouter().setFlags(transaction->getID(), HashRouterFlags::BAD);
return false;
}
// NOTE ximinez - I think this check is redundant,
// but I'm not 100% sure yet.
// If so, only cost is looking up HashRouter flags.
auto const [validity, reason] =
checkValidity(registry_.get().getHashRouter(), sttx, view->rules());
XRPL_ASSERT(
validity == Validity::Valid, "xrpl::NetworkOPsImp::processTransaction : valid validity");
// Not concerned with local checks at this point.
if (validity == Validity::SigBad)
{
JLOG(journal_.info()) << "Transaction has bad signature: " << reason;
transaction->setStatus(TransStatus::INVALID);
transaction->setResult(temBAD_SIGNATURE);
registry_.get().getHashRouter().setFlags(transaction->getID(), HashRouterFlags::BAD);
return false;
}
// canonicalize can change our pointer
registry_.get().getMasterTransaction().canonicalize(&transaction);
return true;
}
void
NetworkOPsImp::processTransaction(
std::shared_ptr<Transaction>& transaction,
bool bUnlimited,
bool bLocal,
FailHard failType)
{
using namespace telemetry;
// SpanGuard is thread-free (holds no Scope), so it is safe to store here
// and end on the batch worker thread that later applies this transaction —
// no detach step is needed.
auto span = std::make_shared<SpanGuard>(txProcessSpan(transaction->getID()));
span->setAttribute(tx_span::attr::txHash, to_string(transaction->getID()).c_str());
span->setAttribute(tx_span::attr::local, bLocal);
// The current (open) ledger index at submission/relay time — the ledger
// being worked on. Correlates this tx.process to the ledger trace; the tx
// has not yet been applied to a specific ledger here, so there is no hash.
span->setAttribute(
tx_span::attr::currentLedgerSeq,
static_cast<std::int64_t>(ledgerMaster_.getCurrentLedgerIndex()));
if (auto const& stx = transaction->getSTransaction())
{
if (auto const* fmt = TxFormats::getInstance().findByType(stx->getTxnType()))
span->setAttribute(tx_span::attr::txType, fmt->getName().c_str());
span->setAttribute(
tx_span::attr::fee, static_cast<int64_t>(stx->getFieldAmount(sfFee).xrp().drops()));
span->setAttribute(
tx_span::attr::sequence, static_cast<int64_t>(stx->getSeqProxy().value()));
}
auto ev = jobQueue_.makeLoadEvent(JtTxnProc, "ProcessTXN");
// preProcessTransaction can change our pointer
if (!preProcessTransaction(transaction))
return;
if (bLocal)
{
span->setAttribute(tx_span::attr::path, tx_span::val::sync);
doTransactionSync(transaction, bUnlimited, failType, std::move(span));
}
else
{
span->setAttribute(tx_span::attr::path, tx_span::val::async);
doTransactionAsync(transaction, bUnlimited, failType, std::move(span));
}
}
void
NetworkOPsImp::doTransactionAsync(
std::shared_ptr<Transaction> transaction,
bool bUnlimited,
FailHard failType,
std::shared_ptr<telemetry::SpanGuard> span)
{
std::scoped_lock const lock(mutex_);
if (transaction->getApplying())
return;
transactions_.emplace_back(transaction, bUnlimited, false, failType, std::move(span));
transaction->setApplying();
if (dispatchState_ == DispatchState::None)
{
if (jobQueue_.addJob(JtBatch, "TxBatchAsync", [this]() { transactionBatch(); }))
{
dispatchState_ = DispatchState::Scheduled;
}
}
}
void
NetworkOPsImp::doTransactionSync(
std::shared_ptr<Transaction> transaction,
bool bUnlimited,
FailHard failType,
std::shared_ptr<telemetry::SpanGuard> span)
{
std::unique_lock<std::mutex> lock(mutex_);
if (!transaction->getApplying())
{
transactions_.emplace_back(transaction, bUnlimited, true, failType, std::move(span));
transaction->setApplying();
}
doTransactionSyncBatch(lock, [&transaction](std::unique_lock<std::mutex> const&) {
return transaction->getApplying();
});
}
void
NetworkOPsImp::doTransactionSyncBatch(
std::unique_lock<std::mutex>& lock,
std::function<bool(std::unique_lock<std::mutex> const&)> retryCallback)
{
do
{
if (dispatchState_ == DispatchState::Running)
{
// A batch processing job is already running, so wait.
cond_.wait(lock);
}
else
{
apply(lock);
if (!transactions_.empty())
{
// More transactions need to be applied, but by another job.
if (jobQueue_.addJob(JtBatch, "TxBatchSync", [this]() { transactionBatch(); }))
{
dispatchState_ = DispatchState::Scheduled;
}
}
}
} while (retryCallback(lock));
}
void
NetworkOPsImp::processTransactionSet(CanonicalTXSet const& set)
{
auto ev = jobQueue_.makeLoadEvent(JtTxnProc, "ProcessTXNSet");
std::vector<std::shared_ptr<Transaction>> candidates;
candidates.reserve(set.size());
for (auto const& [_, tx] : set)
{
std::string reason;
auto transaction = std::make_shared<Transaction>(tx, reason, registry_.get().getApp());
if (transaction->getStatus() == TransStatus::INVALID)
{
if (!reason.empty())
{
JLOG(journal_.trace()) << "Exception checking transaction: " << reason;
}
registry_.get().getHashRouter().setFlags(tx->getTransactionID(), HashRouterFlags::BAD);
continue;
}
// preProcessTransaction can change our pointer
if (!preProcessTransaction(transaction))
continue;
candidates.emplace_back(transaction);
}
std::vector<TransactionStatus> transactions;
transactions.reserve(candidates.size());
std::unique_lock lock(mutex_);
for (auto& transaction : candidates)
{
if (!transaction->getApplying())
{
transactions.emplace_back(transaction, false, false, FailHard::No);
transaction->setApplying();
}
}
if (transactions_.empty())
{
transactions_.swap(transactions);
}
else
{
transactions_.reserve(transactions_.size() + transactions.size());
for (auto& t : transactions)
transactions_.push_back(std::move(t));
}
if (transactions_.empty())
{
JLOG(journal_.debug()) << "No transaction to process!";
return;
}
doTransactionSyncBatch(lock, [&](std::unique_lock<std::mutex> const&) {
XRPL_ASSERT(lock.owns_lock(), "xrpl::NetworkOPsImp::processTransactionSet has lock");
return std::ranges::any_of(
transactions_, [](auto const& t) { return t.transaction->getApplying(); });
});
}
void
NetworkOPsImp::transactionBatch()
{
std::unique_lock<std::mutex> lock(mutex_);
if (dispatchState_ == DispatchState::Running)
return;
while (!transactions_.empty())
{
apply(lock);
}
}
void
NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
{
std::vector<TransactionStatus> submitHeld;
std::vector<TransactionStatus> transactions;
transactions_.swap(transactions);
XRPL_ASSERT(!transactions.empty(), "xrpl::NetworkOPsImp::apply : non-empty transactions");
XRPL_ASSERT(
dispatchState_ != DispatchState::Running, "xrpl::NetworkOPsImp::apply : is not running");
dispatchState_ = DispatchState::Running;
batchLock.unlock();
{
std::unique_lock masterLock{registry_.get().getApp().getMasterMutex(), std::defer_lock};
bool changed = false;
{
std::unique_lock ledgerLock{ledgerMaster_.peekMutex(), std::defer_lock};
std::lock(masterLock, ledgerLock);
registry_.get().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
for (TransactionStatus& e : transactions)
{
// we check before adding to the batch
ApplyFlags flags = TapNone;
if (e.admin)
flags |= TapUnlimited;
if (e.failType == FailHard::Yes)
flags |= TapFailHard;
// Parent the txq.enqueue span to this tx's tx.process span
// via an explicit captured context (the parent is explicit,
// not ambient-inherited). Null on the open-ledger rebuild
// path, where no tx.process span exists.
auto const txProcessCtx =
(e.span && *e.span) ? e.span->spanContext() : telemetry::SpanContext{};
auto const result = registry_.get().getTxQ().apply(
registry_.get().getApp(),
view,
e.transaction->getSTransaction(),
flags,
j,
txProcessCtx.isValid() ? &txProcessCtx : nullptr);
e.result = result.ter;
e.applied = result.applied;
changed = changed || result.applied;
}
return changed;
});
}
if (changed)
reportFeeChange();
std::optional<LedgerIndex> validatedLedgerIndex;
if (auto const l = ledgerMaster_.getValidatedLedger())
validatedLedgerIndex = l->header().seq;
auto newOL = registry_.get().getOpenLedger().current();
for (TransactionStatus const& e : transactions)
{
// Make this transaction's span ambient for the duration of its
// apply so the per-tx log lines below carry its trace_id.
// Non-owning: the span is still owned/ended by e.span. Scoped to
// one loop iteration, so each tx's span is ambient only for its
// own processing. No yield in this loop.
auto txActivation = telemetry::activateIfLive(e.span);
if (e.span && *e.span)
{
e.span->setAttribute(
telemetry::tx_span::attr::terResult, transToken(e.result).c_str());
e.span->setAttribute(telemetry::tx_span::attr::applied, e.applied);
}
e.transaction->clearSubmitResult();
if (e.applied)
{
pubProposedTransaction(newOL, e.transaction->getSTransaction(), e.result);
e.transaction->setApplied();
}
e.transaction->setResult(e.result);
if (isTemMalformed(e.result))
{
registry_.get().getHashRouter().setFlags(
e.transaction->getID(), HashRouterFlags::BAD);
}
#ifdef DEBUG
if (!isTesSuccess(e.result))
{
std::string token, human;
if (transResultInfo(e.result, token, human))
{
JLOG(journal_.info()) << "TransactionResult: " << token << ": " << human;
}
}
#endif
bool const addLocal = e.local;
if (isTesSuccess(e.result))
{
JLOG(journal_.debug()) << "Transaction is now included in open ledger";
e.transaction->setStatus(TransStatus::INCLUDED);
// Pop as many "reasonable" transactions for this account as
// possible. "Reasonable" means they have sequential sequence
// numbers, or use tickets.
auto const& txCur = e.transaction->getSTransaction();
std::size_t count = 0;
for (auto txNext = ledgerMaster_.popAcctTransaction(txCur);
txNext && count < kMaxPoppedTransactions;
txNext = ledgerMaster_.popAcctTransaction(txCur), ++count)
{
if (!batchLock.owns_lock())
batchLock.lock();
std::string reason;
auto const trans = sterilize(*txNext);
auto t = std::make_shared<Transaction>(trans, reason, registry_.get().getApp());
if (t->getApplying())
break;
submitHeld.emplace_back(t, false, false, FailHard::No);
t->setApplying();
}
if (batchLock.owns_lock())
batchLock.unlock();
}
else if (e.result == tefPAST_SEQ)
{
// duplicate or conflict
JLOG(journal_.info()) << "Transaction is obsolete";
e.transaction->setStatus(TransStatus::OBSOLETE);
}
else if (e.result == terQUEUED)
{
JLOG(journal_.debug()) << "Transaction is likely to claim a"
<< " fee, but is queued until fee drops";
e.transaction->setStatus(TransStatus::HELD);
// Add to held transactions, because it could get
// kicked out of the queue, and this will try to
// put it back.
ledgerMaster_.addHeldTransaction(e.transaction);
e.transaction->setQueued();
e.transaction->setKept();
}
else if (isTerRetry(e.result) || isTelLocal(e.result) || isTefFailure(e.result))
{
if (e.failType != FailHard::Yes)
{
auto const lastLedgerSeq =
e.transaction->getSTransaction()->at(~sfLastLedgerSequence);
auto const ledgersLeft = lastLedgerSeq
? *lastLedgerSeq - ledgerMaster_.getCurrentLedgerIndex()
: std::optional<LedgerIndex>{};
// If any of these conditions are met, the transaction can
// be held:
// 1. It was submitted locally. (Note that this flag is only
// true on the initial submission.)
// 2. The transaction has a LastLedgerSequence, and the
// LastLedgerSequence is fewer than LocalTxs::kHoldLedgers
// (5) ledgers into the future. (Remember that an
// unseated optional compares as less than all seated
// values, so it has to be checked explicitly first.)
// 3. The HashRouterFlags::BAD flag is not set on the txID.
// (setFlags
// checks before setting. If the flag is set, it returns
// false, which means it's been held once without one of
// the other conditions, so don't hold it again. Time's
// up!)
//
if (e.local || (ledgersLeft && ledgersLeft <= LocalTxs::kHoldLedgers) ||
registry_.get().getHashRouter().setFlags(
e.transaction->getID(), HashRouterFlags::HELD))
{
// transaction should be held
JLOG(journal_.debug()) << "Transaction should be held: " << e.result;
e.transaction->setStatus(TransStatus::HELD);
ledgerMaster_.addHeldTransaction(e.transaction);
e.transaction->setKept();
}
else
{
JLOG(journal_.debug())
<< "Not holding transaction " << e.transaction->getID() << ": "
<< (e.local ? "local" : "network") << ", "
<< "result: " << e.result << " ledgers left: "
<< (ledgersLeft ? to_string(*ledgersLeft) : "unspecified");
}
}
}
else
{
JLOG(journal_.debug()) << "Status other than success " << e.result;
e.transaction->setStatus(TransStatus::INVALID);
}
auto const enforceFailHard = e.failType == FailHard::Yes && !isTesSuccess(e.result);
if (addLocal && !enforceFailHard)
{
localTX_->pushBack(
ledgerMaster_.getCurrentLedgerIndex(), e.transaction->getSTransaction());
e.transaction->setKept();
}
if ((e.applied ||
((mode_ != OperatingMode::FULL) && (e.failType != FailHard::Yes) && e.local) ||
(e.result == terQUEUED)) &&
!enforceFailHard)
{
auto const toSkip =
registry_.get().getHashRouter().shouldRelay(e.transaction->getID());
if (auto const sttx = *(e.transaction->getSTransaction()); toSkip &&
// Skip relaying if it's an inner batch txn. The flag should
// only be set if the Batch feature is enabled. If Batch is
// not enabled, the flag is always invalid, so don't relay
// it regardless.
!(sttx.isFlag(tfInnerBatchTxn)))
{
protocol::TMTransaction tx;
Serializer s;
sttx.add(s);
tx.set_rawtransaction(s.data(), s.size());
tx.set_status(protocol::tsCURRENT);
tx.set_receivetimestamp(
registry_.get().getTimeKeeper().now().time_since_epoch().count());
tx.set_deferred(e.result == terQUEUED);
// Inject the tx.process span's trace context so the
// receiving node can link its tx.receive span as a child.
if (e.span && *e.span)
telemetry::injectSpanContext(*e.span, *tx.mutable_trace_context());
// FIXME: This should be when we received it
registry_.get().getOverlay().relay(e.transaction->getID(), tx, *toSkip);
e.transaction->setBroadcast();
}
}
if (validatedLedgerIndex)
{
auto [fee, accountSeq, availableSeq] =
registry_.get().getTxQ().getTxRequiredFeeAndSeq(
*newOL, e.transaction->getSTransaction());
e.transaction->setCurrentLedgerState(
*validatedLedgerIndex, fee, accountSeq, availableSeq);
}
}
}
batchLock.lock();
for (TransactionStatus const& e : transactions)
e.transaction->clearApplying();
if (!submitHeld.empty())
{
if (transactions_.empty())
{
transactions_.swap(submitHeld);
}
else
{
transactions_.reserve(transactions_.size() + submitHeld.size());
for (auto& e : submitHeld)
transactions_.push_back(std::move(e));
}
}
cond_.notify_all();
dispatchState_ = DispatchState::None;
}
//
// Owner functions
//
json::Value
NetworkOPsImp::getOwnerInfo(std::shared_ptr<ReadView const> lpLedger, AccountID const& account)
{
json::Value jvObjects(json::ValueType::Object);
auto root = keylet::ownerDir(account);
auto sleNode = lpLedger->read(keylet::page(root));
if (sleNode)
{
std::uint64_t uNodeDir = 0;
do
{
for (auto const& uDirEntry : sleNode->getFieldV256(sfIndexes))
{
auto sleCur = lpLedger->read(keylet::child(uDirEntry));
XRPL_ASSERT(sleCur, "xrpl::NetworkOPsImp::getOwnerInfo : non-null child SLE");
switch (sleCur->getType())
{
case ltOFFER:
if (!jvObjects.isMember(jss::offers))
jvObjects[jss::offers] = json::Value(json::ValueType::Array);
jvObjects[jss::offers].append(sleCur->getJson(JsonOptions::Values::None));
break;
case ltRIPPLE_STATE:
if (!jvObjects.isMember(jss::ripple_lines))
{
jvObjects[jss::ripple_lines] = json::Value(json::ValueType::Array);
}
jvObjects[jss::ripple_lines].append(
sleCur->getJson(JsonOptions::Values::None));
break;
case ltACCOUNT_ROOT:
case ltDIR_NODE:
// LCOV_EXCL_START
default:
UNREACHABLE(
"xrpl::NetworkOPsImp::getOwnerInfo : invalid "
"type");
break;
// LCOV_EXCL_STOP
}
}
uNodeDir = sleNode->getFieldU64(sfIndexNext);
if (uNodeDir != 0u)
{
sleNode = lpLedger->read(keylet::page(root, uNodeDir));
XRPL_ASSERT(sleNode, "xrpl::NetworkOPsImp::getOwnerInfo : read next page");
}
} while (uNodeDir != 0u);
}
return jvObjects;
}
//
// Other
//
inline bool
NetworkOPsImp::isBlocked()
{
return isAmendmentBlocked() || isUNLBlocked();
}
inline bool
NetworkOPsImp::isAmendmentBlocked()
{
return amendmentBlocked_;
}
void
NetworkOPsImp::setAmendmentBlocked()
{
amendmentBlocked_ = true;
setMode(OperatingMode::CONNECTED);
}
inline bool
NetworkOPsImp::isAmendmentWarned()
{
return !amendmentBlocked_ && amendmentWarned_;
}
inline void
NetworkOPsImp::setAmendmentWarned()
{
amendmentWarned_ = true;
}
inline void
NetworkOPsImp::clearAmendmentWarned()
{
amendmentWarned_ = false;
}
inline bool
NetworkOPsImp::isUNLBlocked()
{
return unlBlocked_;
}
void
NetworkOPsImp::setUNLBlocked()
{
unlBlocked_ = true;
setMode(OperatingMode::CONNECTED);
}
inline void
NetworkOPsImp::clearUNLBlocked()
{
unlBlocked_ = false;
}
bool
NetworkOPsImp::checkLastClosedLedger(Overlay::PeerSequence const& peerList, uint256& networkClosed)
{
// Returns true if there's an *abnormal* ledger issue, normal changing in
// TRACKING mode should return false. Do we have sufficient validations for
// our last closed ledger? Or do sufficient nodes agree? And do we have no
// better ledger available? If so, we are either tracking or full.
JLOG(journal_.trace()) << "NetworkOPsImp::checkLastClosedLedger";
auto const ourClosed = ledgerMaster_.getClosedLedger();
if (!ourClosed)
return false;
uint256 closedLedger = ourClosed->header().hash;
uint256 const prevClosedLedger = ourClosed->header().parentHash;
JLOG(journal_.trace()) << "OurClosed: " << closedLedger;
JLOG(journal_.trace()) << "PrevClosed: " << prevClosedLedger;
//-------------------------------------------------------------------------
// Determine preferred last closed ledger
auto& validations = registry_.get().getValidations();
JLOG(journal_.debug()) << "ValidationTrie " << json::Compact(validations.getJsonTrie());
// Will rely on peer LCL if no trusted validations exist
hash_map<uint256, std::uint32_t> peerCounts;
peerCounts[closedLedger] = 0;
if (mode_ >= OperatingMode::TRACKING)
peerCounts[closedLedger]++;
for (auto& peer : peerList)
{
uint256 const peerLedger = peer->getClosedLedgerHash();
if (peerLedger.isNonZero())
++peerCounts[peerLedger];
}
for (auto const& it : peerCounts)
JLOG(journal_.debug()) << "L: " << it.first << " n=" << it.second;
uint256 const preferredLCL = validations.getPreferredLCL(
RCLValidatedLedger{ourClosed, validations.adaptor().journal()},
ledgerMaster_.getValidLedgerIndex(),
peerCounts);
bool switchLedgers = preferredLCL != closedLedger;
if (switchLedgers)
closedLedger = preferredLCL;
//-------------------------------------------------------------------------
if (switchLedgers && (closedLedger == prevClosedLedger))
{
// don't switch to our own previous ledger
JLOG(journal_.info()) << "We won't switch to our own previous ledger";
networkClosed = ourClosed->header().hash;
switchLedgers = false;
}
else
{
networkClosed = closedLedger;
}
if (!switchLedgers)
return false;
auto consensus = ledgerMaster_.getLedgerByHash(closedLedger);
if (!consensus)
{
consensus = registry_.get().getInboundLedgers().acquire(
closedLedger, 0, InboundLedger::Reason::CONSENSUS);
}
if (consensus &&
(!ledgerMaster_.canBeCurrent(consensus) ||
!ledgerMaster_.isCompatible(*consensus, journal_.debug(), "Not switching")))
{
// Don't switch to a ledger not on the validated chain
// or with an invalid close time or sequence
networkClosed = ourClosed->header().hash;
return false;
}
JLOG(journal_.warn()) << "We are not running on the consensus ledger";
JLOG(journal_.info()) << "Our LCL: " << ourClosed->header().hash << getJson({*ourClosed, {}});
JLOG(journal_.info()) << "Net LCL " << closedLedger;
if ((mode_ == OperatingMode::TRACKING) || (mode_ == OperatingMode::FULL))
{
setMode(OperatingMode::CONNECTED);
}
if (consensus)
{
// FIXME: If this rewinds the ledger sequence, or has the same
// sequence, we should update the status on any stored transactions
// in the invalidated ledgers.
switchLastClosedLedger(consensus);
}
return true;
}
void
NetworkOPsImp::switchLastClosedLedger(std::shared_ptr<Ledger const> const& newLCL)
{
// set the newLCL as our last closed ledger -- this is abnormal code
JLOG(journal_.error()) << "JUMP last closed ledger to " << newLCL->header().hash;
// This node was told the network's last closed ledger is not the one it
// built on, and is discarding its own chain tip to follow. Log-only until
// now, so a node repeatedly thrashing between chains left no time series
// to correlate against the rest of the sync pipeline. Rare event, one
// counter Add, no labels: the ledger hash and sequence would both be
// unbounded as label values, and the log line above already carries them.
XRPL_METRIC_COUNTER_INC(
registry_.get(),
telemetry::metric::ledgerJumpTotal,
"Forced jumps of the last closed ledger to a divergent chain");
clearNeedNetworkLedger();
// Update fee computations.
registry_.get().getTxQ().processClosedLedger(registry_.get().getApp(), *newLCL, true);
// Caller must own master lock
{
// Apply tx in old open ledger to new
// open ledger. Then apply local tx.
auto retries = localTX_->getTxSet();
auto const lastVal = registry_.get().getLedgerMaster().getValidatedLedger();
std::optional<Rules> rules;
if (lastVal)
{
rules = makeRulesGivenLedger(*lastVal, registry_.get().getApp().config().features);
}
else
{
rules.emplace(registry_.get().getApp().config().features);
}
registry_.get().getOpenLedger().accept(
registry_.get().getApp(),
*rules,
newLCL,
OrderedTxs({}),
false,
retries,
TapNone,
"jump",
[&](OpenView& view, beast::Journal j) {
// Stuff the ledger with transactions from the queue.
return registry_.get().getTxQ().accept(registry_.get().getApp(), view);
});
}
ledgerMaster_.switchLCL(newLCL);
protocol::TMStatusChange s;
s.set_newevent(protocol::neSWITCHED_LEDGER);
s.set_ledgerseq(newLCL->header().seq);
s.set_networktime(registry_.get().getTimeKeeper().now().time_since_epoch().count());
s.set_ledgerhashprevious(
newLCL->header().parentHash.begin(), newLCL->header().parentHash.size());
s.set_ledgerhash(newLCL->header().hash.begin(), newLCL->header().hash.size());
registry_.get().getOverlay().foreach(
SendAlways(std::make_shared<Message>(s, protocol::mtSTATUS_CHANGE)));
}
bool
NetworkOPsImp::beginConsensus(
uint256 const& networkClosed,
std::unique_ptr<std::stringstream> const& clog)
{
XRPL_ASSERT(networkClosed.isNonZero(), "xrpl::NetworkOPsImp::beginConsensus : nonzero input");
auto closingInfo = ledgerMaster_.getCurrentLedger()->header();
JLOG(journal_.info()) << "Consensus time for #" << closingInfo.seq << " with LCL "
<< closingInfo.parentHash;
auto prevLedger = ledgerMaster_.getLedgerByHash(closingInfo.parentHash);
if (!prevLedger)
{
// this shouldn't happen unless we jump ledgers
if (mode_ == OperatingMode::FULL)
{
JLOG(journal_.warn()) << "Don't have LCL, going to tracking";
setMode(OperatingMode::TRACKING);
CLOG(clog) << "beginConsensus Don't have LCL, going to tracking. ";
}
CLOG(clog) << "beginConsensus no previous ledger. ";
return false;
}
XRPL_ASSERT(
prevLedger->header().hash == closingInfo.parentHash,
"xrpl::NetworkOPsImp::beginConsensus : prevLedger hash matches "
"parent");
XRPL_ASSERT(
closingInfo.parentHash == ledgerMaster_.getClosedLedger()->header().hash,
"xrpl::NetworkOPsImp::beginConsensus : closedLedger parent matches "
"hash");
registry_.get().getValidators().setNegativeUNL(prevLedger->negativeUNL());
TrustChanges const changes = registry_.get().getValidators().updateTrusted(
registry_.get().getValidations().getCurrentNodeIDs(),
closingInfo.parentCloseTime,
*this,
registry_.get().getOverlay(),
registry_.get().getHashRouter());
if (!changes.added.empty() || !changes.removed.empty())
{
registry_.get().getValidations().trustChanged(changes.added, changes.removed);
// Update the AmendmentTable so it tracks the current validators.
registry_.get().getAmendmentTable().trustChanged(
registry_.get().getValidators().getQuorumKeys().second);
}
consensus_.startRound(
registry_.get().getTimeKeeper().closeTime(),
networkClosed,
prevLedger,
changes.removed,
changes.added,
clog);
ConsensusPhase const currPhase = consensus_.phase();
if (lastConsensusPhase_ != currPhase)
{
reportConsensusStateChange(currPhase);
lastConsensusPhase_ = currPhase;
}
JLOG(journal_.debug()) << "Initiating consensus engine";
return true;
}
bool
NetworkOPsImp::processTrustedProposal(RCLCxPeerPos peerPos)
{
auto const& peerKey = peerPos.publicKey();
if (validatorPK_ == peerKey || validatorMasterPK_ == peerKey)
{
// Could indicate a operator misconfiguration where two nodes are
// running with the same validator key configured, so this isn't fatal,
// and it doesn't necessarily indicate peer misbehavior. But since this
// is a trusted message, it could be a very big deal. Either way, we
// don't want to relay the proposal. Note that the byzantine behavior
// detection in handleNewValidation will notify other peers.
//
// Another, innocuous explanation is unusual message routing and delays,
// causing this node to receive its own messages back.
JLOG(journal_.error()) << "Received a proposal signed by MY KEY from a peer. This may "
"indicate a misconfiguration where another node has the same "
"validator key, or may be caused by unusual message routing and "
"delays.";
return false;
}
return consensus_.peerProposal(registry_.get().getTimeKeeper().closeTime(), peerPos);
}
void
NetworkOPsImp::mapComplete(std::shared_ptr<SHAMap> const& map, bool fromAcquire)
{
// We now have an additional transaction set
// Inform peers we have this set
protocol::TMHaveTransactionSet msg;
msg.set_hash(map->getHash().asUInt256().begin(), 256 / 8);
msg.set_status(protocol::tsHAVE);
registry_.get().getOverlay().foreach(
SendAlways(std::make_shared<Message>(msg, protocol::mtHAVE_SET)));
// We acquired it because consensus asked us to
if (fromAcquire)
consensus_.gotTxSet(registry_.get().getTimeKeeper().closeTime(), RCLTxSet{map});
}
void
NetworkOPsImp::endConsensus(std::unique_ptr<std::stringstream> const& clog)
{
uint256 const deadLedger = ledgerMaster_.getClosedLedger()->header().parentHash;
for (auto const& it : registry_.get().getOverlay().getActivePeers())
{
if (it && (it->getClosedLedgerHash() == deadLedger))
{
JLOG(journal_.trace()) << "Killing obsolete peer status";
it->cycleStatus();
}
}
uint256 networkClosed;
bool const ledgerChange =
checkLastClosedLedger(registry_.get().getOverlay().getActivePeers(), networkClosed);
if (networkClosed.isZero())
{
CLOG(clog) << "endConsensus last closed ledger is zero. ";
return;
}
// WRITEME: Unless we are in FULL and in the process of doing a consensus,
// we must count how many nodes share our LCL, how many nodes disagree with
// our LCL, and how many validations our LCL has. We also want to check
// timing to make sure there shouldn't be a newer LCL. We need this
// information to do the next three tests.
if (((mode_ == OperatingMode::CONNECTED) || (mode_ == OperatingMode::SYNCING)) && !ledgerChange)
{
// Count number of peers that agree with us and UNL nodes whose
// validations we have for LCL. If the ledger is good enough, go to
// TRACKING - TODO
if (!needNetworkLedger_)
setMode(OperatingMode::TRACKING);
}
if (((mode_ == OperatingMode::CONNECTED) || (mode_ == OperatingMode::TRACKING)) &&
!ledgerChange)
{
// check if the ledger is good enough to go to FULL
// Note: Do not go to FULL if we don't have the previous ledger
// check if the ledger is bad enough to go to CONNECTED -- TODO
auto current = ledgerMaster_.getCurrentLedger();
if (registry_.get().getTimeKeeper().now() <
(current->header().parentCloseTime + 2 * current->header().closeTimeResolution))
{
setMode(OperatingMode::FULL);
}
}
beginConsensus(networkClosed, clog);
}
void
NetworkOPsImp::consensusViewChange()
{
if ((mode_ == OperatingMode::FULL) || (mode_ == OperatingMode::TRACKING))
{
setMode(OperatingMode::CONNECTED);
}
}
void
NetworkOPsImp::pubManifest(Manifest const& mo)
{
// VFALCO consider std::shared_mutex
std::scoped_lock const sl(subLock_);
if (!streamMaps_[SManifests].empty())
{
json::Value jvObj(json::ValueType::Object);
jvObj[jss::type] = "manifestReceived";
jvObj[jss::master_key] = toBase58(TokenType::NodePublic, mo.masterKey);
if (mo.signingKey)
jvObj[jss::signing_key] = toBase58(TokenType::NodePublic, *mo.signingKey);
jvObj[jss::seq] = json::UInt(mo.sequence);
if (auto sig = mo.getSignature())
jvObj[jss::signature] = strHex(*sig);
jvObj[jss::master_signature] = strHex(mo.getMasterSignature());
if (!mo.domain.empty())
jvObj[jss::domain] = mo.domain;
jvObj[jss::manifest] = strHex(mo.serialized);
for (auto i = streamMaps_[SManifests].begin(); i != streamMaps_[SManifests].end();)
{
if (auto p = i->second.lock())
{
p->send(jvObj, true);
++i;
}
else
{
i = streamMaps_[SManifests].erase(i);
}
}
}
}
NetworkOPsImp::ServerFeeSummary::ServerFeeSummary(
XRPAmount fee,
TxQ::Metrics escalationMetrics, // trivially copyable
LoadFeeTrack const& loadFeeTrack)
: loadFactorServer{loadFeeTrack.getLoadFactor()}
, loadBaseServer{loadFeeTrack.getLoadBase()}
, baseFee{fee}
, em{escalationMetrics}
{
}
bool
NetworkOPsImp::ServerFeeSummary::operator!=(NetworkOPsImp::ServerFeeSummary const& b) const
{
if (loadFactorServer != b.loadFactorServer || loadBaseServer != b.loadBaseServer ||
baseFee != b.baseFee || em.has_value() != b.em.has_value())
return true;
if (em && b.em)
{
return (
em->minProcessingFeeLevel != b.em->minProcessingFeeLevel ||
em->openLedgerFeeLevel != b.em->openLedgerFeeLevel ||
em->referenceFeeLevel != b.em->referenceFeeLevel);
}
return false;
}
// Need to cap to uint64 to uint32 due to JSON limitations
static std::uint32_t
trunc32(std::uint64_t v)
{
constexpr std::uint64_t kMax32 = std::numeric_limits<std::uint32_t>::max();
return std::min(kMax32, v);
};
void
NetworkOPsImp::pubServer()
{
// VFALCO TODO Don't hold the lock across calls to send...make a copy of the
// list into a local array while holding the lock then release
// the lock and call send on everyone.
//
std::scoped_lock const sl(subLock_);
if (!streamMaps_[SServer].empty())
{
json::Value jvObj(json::ValueType::Object);
ServerFeeSummary f{
registry_.get().getOpenLedger().current()->fees().base,
registry_.get().getTxQ().getMetrics(*registry_.get().getOpenLedger().current()),
registry_.get().getFeeTrack()};
jvObj[jss::type] = "serverStatus";
jvObj[jss::server_status] = strOperatingMode();
jvObj[jss::load_base] = f.loadBaseServer;
jvObj[jss::load_factor_server] = f.loadFactorServer;
jvObj[jss::base_fee] = f.baseFee.jsonClipped();
if (f.em)
{
auto const loadFactor = std::max(
safeCast<std::uint64_t>(f.loadFactorServer),
mulDiv(f.em->openLedgerFeeLevel, f.loadBaseServer, f.em->referenceFeeLevel)
.value_or(xrpl::kMuldivMax));
jvObj[jss::load_factor] = trunc32(loadFactor);
jvObj[jss::load_factor_fee_escalation] = f.em->openLedgerFeeLevel.jsonClipped();
jvObj[jss::load_factor_fee_queue] = f.em->minProcessingFeeLevel.jsonClipped();
jvObj[jss::load_factor_fee_reference] = f.em->referenceFeeLevel.jsonClipped();
}
else
{
jvObj[jss::load_factor] = f.loadFactorServer;
}
lastFeeSummary_ = f;
for (auto i = streamMaps_[SServer].begin(); i != streamMaps_[SServer].end();)
{
InfoSub::pointer const p = i->second.lock();
// VFALCO TODO research the possibility of using thread queues and
// linearizing the deletion of subscribers with the
// sending of JSON data.
if (p)
{
p->send(jvObj, true);
++i;
}
else
{
i = streamMaps_[SServer].erase(i);
}
}
}
}
void
NetworkOPsImp::pubConsensus(ConsensusPhase phase)
{
std::scoped_lock const sl(subLock_);
auto& streamMap = streamMaps_[SConsensusPhase];
if (!streamMap.empty())
{
json::Value jvObj(json::ValueType::Object);
jvObj[jss::type] = "consensusPhase";
jvObj[jss::consensus] = to_string(phase);
for (auto i = streamMap.begin(); i != streamMap.end();)
{
if (auto p = i->second.lock())
{
p->send(jvObj, true);
++i;
}
else
{
i = streamMap.erase(i);
}
}
}
}
void
NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> const& val)
{
// VFALCO consider std::shared_mutex
std::scoped_lock const sl(subLock_);
if (!streamMaps_[SValidations].empty())
{
json::Value jvObj(json::ValueType::Object);
auto const signerPublic = val->getSignerPublic();
jvObj[jss::type] = "validationReceived";
jvObj[jss::validation_public_key] = toBase58(TokenType::NodePublic, signerPublic);
jvObj[jss::ledger_hash] = to_string(val->getLedgerHash());
jvObj[jss::signature] = strHex(val->getSignature());
jvObj[jss::full] = val->isFull();
jvObj[jss::flags] = val->getFlags();
jvObj[jss::signing_time] = *(*val)[~sfSigningTime];
jvObj[jss::data] = strHex(val->getSerializer().slice());
jvObj[jss::network_id] = registry_.get().getNetworkIDService().getNetworkID();
if (auto version = (*val)[~sfServerVersion])
jvObj[jss::server_version] = std::to_string(*version);
if (auto cookie = (*val)[~sfCookie])
jvObj[jss::cookie] = std::to_string(*cookie);
if (auto hash = (*val)[~sfValidatedHash])
jvObj[jss::validated_hash] = strHex(*hash);
auto const masterKey = registry_.get().getValidatorManifests().getMasterKey(signerPublic);
if (masterKey != signerPublic)
jvObj[jss::master_key] = toBase58(TokenType::NodePublic, masterKey);
// NOTE *seq is a number, but old API versions used string. We replace
// number with a string using MultiApiJson near end of this function
if (auto const seq = (*val)[~sfLedgerSequence])
jvObj[jss::ledger_index] = *seq;
if (val->isFieldPresent(sfAmendments))
{
jvObj[jss::amendments] = json::Value(json::ValueType::Array);
for (auto const& amendment : val->getFieldV256(sfAmendments))
jvObj[jss::amendments].append(to_string(amendment));
}
if (auto const closeTime = (*val)[~sfCloseTime])
jvObj[jss::close_time] = *closeTime;
if (auto const loadFee = (*val)[~sfLoadFee])
jvObj[jss::load_fee] = *loadFee;
if (auto const baseFee = val->at(~sfBaseFee))
jvObj[jss::base_fee] = static_cast<double>(*baseFee);
if (auto const reserveBase = val->at(~sfReserveBase))
jvObj[jss::reserve_base] = *reserveBase;
if (auto const reserveInc = val->at(~sfReserveIncrement))
jvObj[jss::reserve_inc] = *reserveInc;
// (The ~ operator converts the Proxy to a std::optional, which
// simplifies later operations)
if (auto const baseFeeXRP = ~val->at(~sfBaseFeeDrops); baseFeeXRP && baseFeeXRP->native())
jvObj[jss::base_fee] = baseFeeXRP->xrp().jsonClipped();
if (auto const reserveBaseXRP = ~val->at(~sfReserveBaseDrops);
reserveBaseXRP && reserveBaseXRP->native())
jvObj[jss::reserve_base] = reserveBaseXRP->xrp().jsonClipped();
if (auto const reserveIncXRP = ~val->at(~sfReserveIncrementDrops);
reserveIncXRP && reserveIncXRP->native())
jvObj[jss::reserve_inc] = reserveIncXRP->xrp().jsonClipped();
// NOTE Use MultiApiJson to publish two slightly different JSON objects
// for consumers supporting different API versions
MultiApiJson multiObj{jvObj};
multiObj.visit(
RPC::kApiVersion<1>, //
[](json::Value& jvTx) {
// Type conversion for older API versions to string
if (jvTx.isMember(jss::ledger_index))
{
jvTx[jss::ledger_index] = std::to_string(jvTx[jss::ledger_index].asUInt());
}
});
for (auto i = streamMaps_[SValidations].begin(); i != streamMaps_[SValidations].end();)
{
if (auto p = i->second.lock())
{
multiObj.visit(
p->getApiVersion(), //
[&](json::Value const& jv) { p->send(jv, true); });
++i;
}
else
{
i = streamMaps_[SValidations].erase(i);
}
}
}
}
void
NetworkOPsImp::pubPeerStatus(std::function<json::Value(void)> const& func)
{
std::scoped_lock const sl(subLock_);
if (!streamMaps_[SPeerStatus].empty())
{
json::Value jvObj(func());
jvObj[jss::type] = "peerStatusChange";
for (auto i = streamMaps_[SPeerStatus].begin(); i != streamMaps_[SPeerStatus].end();)
{
InfoSub::pointer const p = i->second.lock();
if (p)
{
p->send(jvObj, true);
++i;
}
else
{
i = streamMaps_[SPeerStatus].erase(i);
}
}
}
}
void
NetworkOPsImp::setMode(OperatingMode om)
{
using namespace std::chrono_literals;
if (om == OperatingMode::CONNECTED)
{
if (registry_.get().getLedgerMaster().getValidatedLedgerAge() < 1min)
om = OperatingMode::SYNCING;
}
else if (om == OperatingMode::SYNCING)
{
if (registry_.get().getLedgerMaster().getValidatedLedgerAge() >= 1min)
om = OperatingMode::CONNECTED;
}
if ((om > OperatingMode::CONNECTED) && isBlocked())
om = OperatingMode::CONNECTED;
if (mode_ == om)
return;
// Capture the mode we are leaving before overwriting it: the transition
// edge, not just the destination, is what tells flapping apart from a
// clean climb to FULL.
auto const prevMode = mode_.load();
mode_ = om;
accounting_.mode(om);
// Record the mode transition labelled with source and destination, so the
// dashboard can chart which edges of the sync state machine are traversed
// (e.g. repeated full->connected flapping vs. a one-way
// tracking->connected->full climb). Only reached on a real mode change,
// never in a hot loop, and there are only five modes so the label
// cardinality is bounded. strOperatingMode(mode, admin=false) supplies the
// names, which keeps them identical to the ones server_info reports.
XRPL_METRIC_COUNTER_INC_LABELED(
registry_.get(),
telemetry::metric::stateChangesTotal,
"Total operating mode changes",
{{telemetry::label::from, strOperatingMode(prevMode, false)},
{telemetry::label::to, strOperatingMode(om, false)}});
JLOG(journal_.info()) << "STATE->" << strOperatingMode();
pubServer();
}
bool
NetworkOPsImp::recvValidation(std::shared_ptr<STValidation> const& val, std::string const& source)
{
JLOG(journal_.trace()) << "recvValidation " << val->getLedgerHash() << " from " << source;
if (auto* mr = registry_.get().getMetricsRegistry())
mr->incrementValidationsChecked();
std::unique_lock lock(validationsMutex_);
BypassAccept bypassAccept = BypassAccept::No;
try
{
if (pendingValidations_.contains(val->getLedgerHash()))
{
bypassAccept = BypassAccept::Yes;
}
else
{
pendingValidations_.insert(val->getLedgerHash());
}
ScopeUnlock const unlock(lock);
handleNewValidation(registry_.get().getApp(), val, source, bypassAccept, journal_);
}
catch (std::exception const& e)
{
JLOG(journal_.warn()) << "Exception thrown for handling new validation "
<< val->getLedgerHash() << ": " << e.what();
}
catch (...)
{
JLOG(journal_.warn()) << "Unknown exception thrown for handling new validation "
<< val->getLedgerHash();
}
if (bypassAccept == BypassAccept::No)
{
pendingValidations_.erase(val->getLedgerHash());
}
lock.unlock();
pubValidation(val);
JLOG(journal_.debug()) << [this, &val]() -> auto {
std::stringstream ss;
ss << "VALIDATION: " << val->render() << " master_key: ";
auto master = registry_.get().getValidators().getTrustedKey(val->getSignerPublic());
if (master)
{
ss << toBase58(TokenType::NodePublic, *master);
}
else
{
ss << "none";
}
return ss.str();
}();
// We will always relay trusted validations; if configured, we will
// also relay all untrusted validations.
return registry_.get().getApp().config().relayUntrustedValidations == 1 || val->isTrusted();
}
json::Value
NetworkOPsImp::getConsensusInfo()
{
return consensus_.getJson(true);
}
json::Value
NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
{
json::Value info = json::ValueType::Object;
// System-level warnings
{
json::Value warnings{json::ValueType::Array};
if (isAmendmentBlocked())
{
json::Value& w = warnings.append(json::ValueType::Object);
w[jss::id] = WarnRpcAmendmentBlocked;
w[jss::message] =
"This server is amendment blocked, and must be updated to be "
"able to stay in sync with the network.";
}
if (isUNLBlocked())
{
json::Value& w = warnings.append(json::ValueType::Object);
w[jss::id] = WarnRpcExpiredValidatorList;
w[jss::message] =
"This server has an expired validator list. validators.txt "
"may be incorrectly configured or some [validator_list_sites] "
"may be unreachable.";
}
if (admin && isAmendmentWarned())
{
json::Value& w = warnings.append(json::ValueType::Object);
w[jss::id] = WarnRpcUnsupportedMajority;
w[jss::message] =
"One or more unsupported amendments have reached majority. "
"Upgrade to the latest version before they are activated "
"to avoid being amendment blocked.";
if (auto const expected =
registry_.get().getAmendmentTable().firstUnsupportedExpected())
{
auto& d = w[jss::details] = json::ValueType::Object;
d[jss::expected_date] = expected->time_since_epoch().count();
d[jss::expected_date_UTC] = to_string(*expected);
}
}
if (warnings.size() != 0u)
info[jss::warnings] = std::move(warnings);
}
// hostid: unique string describing the machine
if (human)
info[jss::hostid] = getHostId(admin);
// domain: if configured with a domain, report it:
if (!registry_.get().getApp().config().serverDomain.empty())
info[jss::server_domain] = registry_.get().getApp().config().serverDomain;
info[jss::build_version] = BuildInfo::getVersionString();
info[jss::server_state] = strOperatingMode(admin);
info[jss::time] =
to_string(std::chrono::floor<std::chrono::microseconds>(std::chrono::system_clock::now()));
if (needNetworkLedger_)
info[jss::network_ledger] = "waiting";
info[jss::validation_quorum] =
static_cast<json::UInt>(registry_.get().getValidators().quorum());
if (admin)
{
// Note: By default the node size is "tiny". When parsing it's an error if the final
// NODE_SIZE is over 4 so below code should be safe.
// NOLINTNEXTLINE(bugprone-switch-missing-default-case)
switch (registry_.get().getApp().config().nodeSize)
{
case 0:
info[jss::node_size] = "tiny";
break;
case 1:
info[jss::node_size] = "small";
break;
case 2:
info[jss::node_size] = "medium";
break;
case 3:
info[jss::node_size] = "large";
break;
case 4:
info[jss::node_size] = "huge";
break;
}
auto when = registry_.get().getValidators().expires();
if (!human)
{
if (when)
{
info[jss::validator_list_expires] =
safeCast<json::UInt>(when->time_since_epoch().count());
}
else
{
info[jss::validator_list_expires] = 0;
}
}
else
{
auto& x = (info[jss::validator_list] = json::ValueType::Object);
x[jss::count] = static_cast<json::UInt>(registry_.get().getValidators().count());
if (when)
{
if (*when == TimeKeeper::time_point::max())
{
x[jss::expiration] = "never";
x[jss::status] = "active";
}
else
{
x[jss::expiration] = to_string(*when);
if (*when > registry_.get().getTimeKeeper().now())
{
x[jss::status] = "active";
}
else
{
x[jss::status] = "expired";
}
}
}
else
{
x[jss::status] = "unknown";
x[jss::expiration] = "unknown";
}
}
if (!xrpl::git::getCommitHash().empty() || !xrpl::git::getBuildBranch().empty())
{
auto& x = (info[jss::git] = json::ValueType::Object);
if (!xrpl::git::getCommitHash().empty())
x[jss::hash] = xrpl::git::getCommitHash();
if (!xrpl::git::getBuildBranch().empty())
x[jss::branch] = xrpl::git::getBuildBranch();
}
}
info[jss::io_latency_ms] =
static_cast<json::UInt>(registry_.get().getApp().getIOLatency().count());
if (admin)
{
if (auto const localPubKey = registry_.get().getValidators().localPublicKey();
localPubKey && registry_.get().getApp().getValidationPublicKey())
{
info[jss::pubkey_validator] = toBase58(TokenType::NodePublic, localPubKey.value());
}
else
{
info[jss::pubkey_validator] = "none";
}
}
if (counters)
{
info[jss::counters] = registry_.get().getPerfLog().countersJson();
json::Value nodestore(json::ValueType::Object);
registry_.get().getNodeStore().getCountsJson(nodestore);
info[jss::counters][jss::nodestore] = nodestore;
info[jss::current_activities] = registry_.get().getPerfLog().currentJson();
}
info[jss::pubkey_node] =
toBase58(TokenType::NodePublic, registry_.get().getApp().nodeIdentity().first);
info[jss::complete_ledgers] = registry_.get().getLedgerMaster().getCompleteLedgers();
if (amendmentBlocked_)
info[jss::amendment_blocked] = true;
auto const fp = ledgerMaster_.getFetchPackCacheSize();
if (fp != 0)
info[jss::fetch_pack] = json::UInt(fp);
info[jss::peers] = json::UInt(registry_.get().getOverlay().size());
json::Value lastClose = json::ValueType::Object;
lastClose[jss::proposers] = json::UInt(consensus_.prevProposers());
if (human)
{
lastClose[jss::converge_time_s] =
std::chrono::duration<double>{consensus_.prevRoundTime()}.count();
}
else
{
lastClose[jss::converge_time] = json::Int(consensus_.prevRoundTime().count());
}
info[jss::last_close] = lastClose;
// info[jss::consensus] = consensus_.getJson();
if (admin)
info[jss::load] = jobQueue_.getJson();
if (auto const netid = registry_.get().getOverlay().networkID())
info[jss::network_id] = static_cast<json::UInt>(*netid);
auto const escalationMetrics =
registry_.get().getTxQ().getMetrics(*registry_.get().getOpenLedger().current());
auto const loadFactorServer = registry_.get().getFeeTrack().getLoadFactor();
auto const loadBaseServer = registry_.get().getFeeTrack().getLoadBase();
/* Scale the escalated fee level to unitless "load factor".
In practice, this just strips the units, but it will continue
to work correctly if either base value ever changes. */
auto const loadFactorFeeEscalation = mulDiv(
escalationMetrics.openLedgerFeeLevel,
loadBaseServer,
escalationMetrics.referenceFeeLevel)
.value_or(xrpl::kMuldivMax);
auto const loadFactor =
std::max(safeCast<std::uint64_t>(loadFactorServer), loadFactorFeeEscalation);
if (!human)
{
info[jss::load_base] = loadBaseServer;
info[jss::load_factor] = trunc32(loadFactor);
info[jss::load_factor_server] = loadFactorServer;
/* json::Value doesn't support uint64, so clamp to max
uint32 value. This is mostly theoretical, since there
probably isn't enough extant XRP to drive the factor
that high.
*/
info[jss::load_factor_fee_escalation] = escalationMetrics.openLedgerFeeLevel.jsonClipped();
info[jss::load_factor_fee_queue] = escalationMetrics.minProcessingFeeLevel.jsonClipped();
info[jss::load_factor_fee_reference] = escalationMetrics.referenceFeeLevel.jsonClipped();
}
else
{
info[jss::load_factor] = static_cast<double>(loadFactor) / loadBaseServer;
if (loadFactorServer != loadFactor)
info[jss::load_factor_server] = static_cast<double>(loadFactorServer) / loadBaseServer;
if (admin)
{
std::uint32_t fee = registry_.get().getFeeTrack().getLocalFee();
if (fee != loadBaseServer)
info[jss::load_factor_local] = static_cast<double>(fee) / loadBaseServer;
fee = registry_.get().getFeeTrack().getRemoteFee();
if (fee != loadBaseServer)
info[jss::load_factor_net] = static_cast<double>(fee) / loadBaseServer;
fee = registry_.get().getFeeTrack().getClusterFee();
if (fee != loadBaseServer)
info[jss::load_factor_cluster] = static_cast<double>(fee) / loadBaseServer;
}
if (escalationMetrics.openLedgerFeeLevel != escalationMetrics.referenceFeeLevel &&
(admin || loadFactorFeeEscalation != loadFactor))
{
info[jss::load_factor_fee_escalation] =
escalationMetrics.openLedgerFeeLevel.decimalFromReference(
escalationMetrics.referenceFeeLevel);
}
if (escalationMetrics.minProcessingFeeLevel != escalationMetrics.referenceFeeLevel)
{
info[jss::load_factor_fee_queue] =
escalationMetrics.minProcessingFeeLevel.decimalFromReference(
escalationMetrics.referenceFeeLevel);
}
}
bool valid = false;
auto lpClosed = ledgerMaster_.getValidatedLedger();
if (lpClosed)
{
valid = true;
}
else
{
lpClosed = ledgerMaster_.getClosedLedger();
}
if (lpClosed)
{
XRPAmount const baseFee = lpClosed->fees().base;
json::Value l(json::ValueType::Object);
l[jss::seq] = json::UInt(lpClosed->header().seq);
l[jss::hash] = to_string(lpClosed->header().hash);
if (!human)
{
l[jss::base_fee] = baseFee.jsonClipped();
l[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped();
l[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped();
l[jss::close_time] =
json::Value::UInt(lpClosed->header().closeTime.time_since_epoch().count());
}
else
{
l[jss::base_fee_xrp] = baseFee.decimalXRP();
l[jss::reserve_base_xrp] = lpClosed->fees().reserve.decimalXRP();
l[jss::reserve_inc_xrp] = lpClosed->fees().increment.decimalXRP();
if (auto const closeOffset = registry_.get().getTimeKeeper().closeOffset();
std::abs(closeOffset.count()) >= 60)
l[jss::close_time_offset] = static_cast<std::uint32_t>(closeOffset.count());
static constexpr std::chrono::seconds kHighAgeThreshold{1000000};
if (ledgerMaster_.haveValidated())
{
auto const age = ledgerMaster_.getValidatedLedgerAge();
l[jss::age] = json::UInt(age < kHighAgeThreshold ? age.count() : 0);
}
else
{
auto lCloseTime = lpClosed->header().closeTime;
auto closeTime = registry_.get().getTimeKeeper().closeTime();
if (lCloseTime <= closeTime)
{
using namespace std::chrono_literals;
auto age = closeTime - lCloseTime;
l[jss::age] = json::UInt(age < kHighAgeThreshold ? age.count() : 0);
}
}
}
if (valid)
{
info[jss::validated_ledger] = l;
}
else
{
info[jss::closed_ledger] = l;
}
auto lpPublished = ledgerMaster_.getPublishedLedger();
if (!lpPublished)
{
info[jss::published_ledger] = "none";
}
else if (lpPublished->header().seq != lpClosed->header().seq)
{
info[jss::published_ledger] = lpPublished->header().seq;
}
}
accounting_.json(info);
info[jss::uptime] = UptimeClock::now().time_since_epoch().count();
info[jss::jq_trans_overflow] =
std::to_string(registry_.get().getOverlay().getJqTransOverflow());
info[jss::peer_disconnects] = std::to_string(registry_.get().getOverlay().getPeerDisconnect());
info[jss::peer_disconnects_resources] =
std::to_string(registry_.get().getOverlay().getPeerDisconnectCharges());
// This array must be sorted in increasing order.
static constexpr std::array<std::string_view, 7> kProtocols{
"http", "https", "peer", "ws", "ws2", "wss", "wss2"};
static_assert(std::ranges::is_sorted(kProtocols));
{
json::Value ports{json::ValueType::Array};
for (auto const& port : registry_.get().getServerHandler().setup().ports)
{
// Don't publish admin ports for non-admin users
if (!admin &&
!(port.adminNetsV4.empty() && port.adminNetsV6.empty() && port.adminUser.empty() &&
port.adminPassword.empty()))
continue;
std::vector<std::string> proto;
// NOLINTNEXTLINE(modernize-use-ranges)
std::set_intersection(
std::begin(port.protocol),
std::end(port.protocol),
std::begin(kProtocols),
std::end(kProtocols),
std::back_inserter(proto));
if (!proto.empty())
{
auto& jv = ports.append(json::Value(json::ValueType::Object));
jv[jss::port] = std::to_string(port.port);
jv[jss::protocol] = json::Value{json::ValueType::Array};
for (auto const& p : proto)
jv[jss::protocol].append(p);
}
}
if (registry_.get().getApp().config().exists(Sections::kPortGrpc))
{
auto const& grpcSection =
registry_.get().getApp().config().section(Sections::kPortGrpc);
auto const optPort = grpcSection.get(Keys::kPort);
if (optPort && grpcSection.get(Keys::kIp))
{
auto& jv = ports.append(json::Value(json::ValueType::Object));
jv[jss::port] = *optPort;
jv[jss::protocol] = json::Value{json::ValueType::Array};
jv[jss::protocol].append("grpc");
}
}
info[jss::ports] = std::move(ports);
}
return info;
}
void
NetworkOPsImp::clearLedgerFetch()
{
registry_.get().getInboundLedgers().clearFailures();
}
json::Value
NetworkOPsImp::getLedgerFetchInfo()
{
return registry_.get().getInboundLedgers().getInfo();
}
void
NetworkOPsImp::pubProposedTransaction(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const& transaction,
TER result)
{
// never publish an inner txn inside a batch txn. The flag should
// only be set if the Batch feature is enabled. If Batch is not
// enabled, the flag is always invalid, so don't publish it
// regardless.
if (transaction->isFlag(tfInnerBatchTxn))
return;
MultiApiJson const jvObj = transJson(transaction, result, false, ledger, std::nullopt);
{
std::scoped_lock const sl(subLock_);
auto it = streamMaps_[SRtTransactions].begin();
while (it != streamMaps_[SRtTransactions].end())
{
InfoSub::pointer p = it->second.lock();
if (p)
{
jvObj.visit(
p->getApiVersion(), //
[&](json::Value const& jv) { p->send(jv, true); });
++it;
}
else
{
it = streamMaps_[SRtTransactions].erase(it);
}
}
}
pubProposedAccountTransaction(ledger, transaction, result);
}
void
NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
{
// Ledgers are published only when they acquire sufficient validations
// Holes are filled across connection loss or other catastrophe
std::shared_ptr<AcceptedLedger> alpAccepted =
registry_.get().getAcceptedLedgerCache().fetch(lpAccepted->header().hash);
if (!alpAccepted)
{
alpAccepted = std::make_shared<AcceptedLedger>(lpAccepted);
registry_.get().getAcceptedLedgerCache().canonicalizeReplaceClient(
lpAccepted->header().hash, alpAccepted);
}
XRPL_ASSERT(
alpAccepted->getLedger().get() == lpAccepted.get(),
"xrpl::NetworkOPsImp::pubLedger : accepted input");
{
JLOG(journal_.debug()) << "Publishing ledger " << lpAccepted->header().seq << " "
<< lpAccepted->header().hash;
std::scoped_lock const sl(subLock_);
if (!streamMaps_[SLedger].empty())
{
json::Value jvObj(json::ValueType::Object);
jvObj[jss::type] = "ledgerClosed";
jvObj[jss::ledger_index] = lpAccepted->header().seq;
jvObj[jss::ledger_hash] = to_string(lpAccepted->header().hash);
jvObj[jss::ledger_time] =
json::Value::UInt(lpAccepted->header().closeTime.time_since_epoch().count());
jvObj[jss::network_id] = registry_.get().getNetworkIDService().getNetworkID();
if (!lpAccepted->rules().enabled(featureXRPFees))
jvObj[jss::fee_ref] = kFeeUnitsDeprecated;
jvObj[jss::fee_base] = lpAccepted->fees().base.jsonClipped();
jvObj[jss::reserve_base] = lpAccepted->fees().reserve.jsonClipped();
jvObj[jss::reserve_inc] = lpAccepted->fees().increment.jsonClipped();
jvObj[jss::txn_count] = json::UInt(alpAccepted->size());
if (mode_ >= OperatingMode::SYNCING)
{
jvObj[jss::validated_ledgers] =
registry_.get().getLedgerMaster().getCompleteLedgers();
}
auto it = streamMaps_[SLedger].begin();
while (it != streamMaps_[SLedger].end())
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
p->send(jvObj, true);
++it;
}
else
{
it = streamMaps_[SLedger].erase(it);
}
}
}
if (!streamMaps_[SBookChanges].empty())
{
json::Value const jvObj = xrpl::RPC::computeBookChanges(lpAccepted);
auto it = streamMaps_[SBookChanges].begin();
while (it != streamMaps_[SBookChanges].end())
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
p->send(jvObj, true);
++it;
}
else
{
it = streamMaps_[SBookChanges].erase(it);
}
}
}
{
static bool kFirstTime = true;
if (kFirstTime)
{
// First validated ledger, start delayed SubAccountHistory
kFirstTime = false;
for (auto& outer : subAccountHistory_)
{
for (auto& inner : outer.second)
{
auto& subInfo = inner.second;
if (subInfo.index->separationLedgerSeq == 0)
{
subAccountHistoryStart(alpAccepted->getLedger(), subInfo);
}
}
}
}
}
}
// Don't lock since pubAcceptedTransaction is locking.
for (auto const& accTx : *alpAccepted)
{
JLOG(journal_.trace()) << "pubAccepted: " << accTx->getJson();
pubValidatedTransaction(lpAccepted, *accTx, accTx == *(--alpAccepted->end()));
}
}
void
NetworkOPsImp::reportFeeChange()
{
ServerFeeSummary const f{
registry_.get().getOpenLedger().current()->fees().base,
registry_.get().getTxQ().getMetrics(*registry_.get().getOpenLedger().current()),
registry_.get().getFeeTrack()};
// only schedule the job if something has changed
if (f != lastFeeSummary_)
{
jobQueue_.addJob(JtClientFeeChange, "PubFee", [this]() { pubServer(); });
}
}
void
NetworkOPsImp::reportConsensusStateChange(ConsensusPhase phase)
{
jobQueue_.addJob(JtClientConsensus, "PubCons", [this, phase]() { pubConsensus(phase); });
}
inline void
NetworkOPsImp::updateLocalTx(ReadView const& view)
{
localTX_->sweep(view);
}
inline std::size_t
NetworkOPsImp::getLocalTxCount()
{
return localTX_->size();
}
std::size_t
NetworkOPsImp::getBookSubscribersCount()
{
std::scoped_lock const sl(subLock_);
std::size_t total = 0;
for (auto const& [_, subs] : subBook_)
total += subs.size();
return total;
}
// This routine should only be used to publish accepted or validated
// transactions.
MultiApiJson
NetworkOPsImp::transJson(
std::shared_ptr<STTx const> const& transaction,
TER result,
bool validated,
std::shared_ptr<ReadView const> const& ledger,
std::optional<std::reference_wrapper<TxMeta const>> meta)
{
json::Value jvObj(json::ValueType::Object);
std::string sToken;
std::string sHuman;
transResultInfo(result, sToken, sHuman);
jvObj[jss::type] = "transaction";
// NOTE jvObj is not a finished object for either API version. After
// it's populated, we need to finish it for a specific API version. This is
// done in a loop, near the end of this function.
jvObj[jss::transaction] = transaction->getJson(JsonOptions::Values::DisableApiPriorV2, false);
if (meta)
{
jvObj[jss::meta] = meta->get().getJson(JsonOptions::Values::None);
RPC::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get());
RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get());
RPC::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get());
}
// add CTID where the needed data for it exists
if (auto const& lookup = ledger->txRead(transaction->getTransactionID());
lookup.second && lookup.second->isFieldPresent(sfTransactionIndex))
{
uint32_t const txnSeq = lookup.second->getFieldU32(sfTransactionIndex);
uint32_t netID = registry_.get().getNetworkIDService().getNetworkID();
if (transaction->isFieldPresent(sfNetworkID))
netID = transaction->getFieldU32(sfNetworkID);
if (std::optional<std::string> ctid = RPC::encodeCTID(ledger->header().seq, txnSeq, netID);
ctid)
jvObj[jss::ctid] = *ctid;
}
if (!ledger->open())
jvObj[jss::ledger_hash] = to_string(ledger->header().hash);
if (validated)
{
jvObj[jss::ledger_index] = ledger->header().seq;
jvObj[jss::transaction][jss::date] = ledger->header().closeTime.time_since_epoch().count();
jvObj[jss::validated] = true;
jvObj[jss::close_time_iso] = toStringIso(ledger->header().closeTime);
// WRITEME: Put the account next seq here
}
else
{
jvObj[jss::validated] = false;
jvObj[jss::ledger_current_index] = ledger->header().seq;
}
jvObj[jss::status] = validated ? "closed" : "proposed";
jvObj[jss::engine_result] = sToken;
jvObj[jss::engine_result_code] = result;
jvObj[jss::engine_result_message] = sHuman;
if (transaction->getTxnType() == ttOFFER_CREATE)
{
auto const account = transaction->getAccountID(sfAccount);
auto const amount = transaction->getFieldAmount(sfTakerGets);
// If the offer create is not self funded then add the owner balance
if (account != amount.getIssuer())
{
auto const ownerFunds = accountFunds(
*ledger,
account,
amount,
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
registry_.get().getJournal("View"));
jvObj[jss::transaction][jss::owner_funds] = ownerFunds.getText();
}
}
std::string const hash = to_string(transaction->getTransactionID());
MultiApiJson multiObj{jvObj};
forAllApiVersions(
multiObj.visit(), //
[&]<unsigned Version>(json::Value& jvTx, std::integral_constant<unsigned, Version>) {
RPC::insertDeliverMax(jvTx[jss::transaction], transaction->getTxnType(), Version);
if constexpr (Version > 1)
{
jvTx[jss::tx_json] = jvTx.removeMember(jss::transaction);
jvTx[jss::hash] = hash;
}
else
{
jvTx[jss::transaction][jss::hash] = hash;
}
});
return multiObj;
}
void
NetworkOPsImp::pubValidatedTransaction(
std::shared_ptr<ReadView const> const& ledger,
AcceptedLedgerTx const& transaction,
bool last)
{
auto const& stTxn = transaction.getTxn();
// Create two different Json objects, for different API versions
auto const metaRef = std::ref(transaction.getMeta());
auto const trResult = transaction.getResult();
MultiApiJson const jvObj = transJson(stTxn, trResult, true, ledger, metaRef);
{
std::scoped_lock const sl(subLock_);
auto it = streamMaps_[STransactions].begin();
while (it != streamMaps_[STransactions].end())
{
InfoSub::pointer p = it->second.lock();
if (p)
{
jvObj.visit(
p->getApiVersion(), //
[&](json::Value const& jv) { p->send(jv, true); });
++it;
}
else
{
it = streamMaps_[STransactions].erase(it);
}
}
it = streamMaps_[SRtTransactions].begin();
while (it != streamMaps_[SRtTransactions].end())
{
InfoSub::pointer p = it->second.lock();
if (p)
{
jvObj.visit(
p->getApiVersion(), //
[&](json::Value const& jv) { p->send(jv, true); });
++it;
}
else
{
it = streamMaps_[SRtTransactions].erase(it);
}
}
}
if (transaction.getResult() == tesSUCCESS)
pubBookTransaction(transaction, jvObj);
pubAccountTransaction(ledger, transaction, last);
}
void
NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson const& jvObj)
{
auto const books = affectedBooks(alTx, journal_);
if (books.empty())
return;
// Two-pass design:
//
// 1. Under subLock_, walk subBook_, collect a strong pointer for each
// unique listener (and prune any expired weak_ptrs we encounter).
// 2. Release subLock_, then send to each collected listener.
//
// Reasoning:
// * send() can be slow / blocking, so holding subLock_ across it would
// stall every other sub/unsub/pub path on this server (see the matching
// TODO above pubServer at line ~2275).
// * A strong pointer destructed while subLock_ is held risks running
// ~InfoSub() in-line, which re-enters unsubBook() and mutates the very
// subBook_/SubMapType being iterated -> dangling iterator UB.
//
// Releasing subLock_ before any InfoSub::pointer can decay solves both.
// ~InfoSub() reacquires subLock_ via unsubBook() on its own and serializes
// safely with concurrent traffic.
std::vector<InfoSub::pointer> listeners;
hash_set<std::uint64_t> seen;
// Sized for the common case where every affected book has at most
// one subscriber. Multi-subscriber books trigger reallocation, but
// that is rare and the upper-bound estimate (sum of per-book sizes)
// would itself require walking subBook_ twice.
listeners.reserve(books.size());
seen.reserve(books.size());
{
std::scoped_lock const sl(subLock_);
for (auto const& book : books)
{
auto it = subBook_.find(book);
if (it == subBook_.end())
continue;
for (auto sit = it->second.begin(); sit != it->second.end();)
{
if (auto p = sit->second.lock())
{
// Defensive: subBook_ entries are normally cleared by
// ~InfoSub() -> unsubBook(), so we rarely see expired
// weak_ptrs here. The else branch covers the narrow race
// where the last strong ref is dropped between insertion
// and our lock() call.
if (seen.emplace(p->getSeq()).second)
listeners.emplace_back(std::move(p));
++sit;
}
else
{
JLOG(journal_.debug())
<< "pubBookTransaction: pruning expired weak_ptr for seq=" << sit->first;
sit = it->second.erase(sit);
}
}
if (it->second.empty())
subBook_.erase(it);
}
}
for (auto const& p : listeners)
{
jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); });
}
// listeners destructs here, outside subLock_; ~InfoSub (if any fires)
// will reacquire subLock_ via unsubBook with no iterator hazard.
}
void
NetworkOPsImp::pubAccountTransaction(
std::shared_ptr<ReadView const> const& ledger,
AcceptedLedgerTx const& transaction,
bool last)
{
hash_set<InfoSub::pointer> notify;
int iProposed = 0;
int iAccepted = 0;
std::vector<SubAccountHistoryInfo> accountHistoryNotify;
auto const currLedgerSeq = ledger->seq();
{
std::scoped_lock const sl(subLock_);
if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty())
{
for (auto const& affectedAccount : transaction.getAffected())
{
if (auto simiIt = subRTAccount_.find(affectedAccount);
simiIt != subRTAccount_.end())
{
auto it = simiIt->second.begin();
while (it != simiIt->second.end())
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
notify.insert(p);
++it;
++iProposed;
}
else
{
it = simiIt->second.erase(it);
}
}
}
if (auto simiIt = subAccount_.find(affectedAccount); simiIt != subAccount_.end())
{
auto it = simiIt->second.begin();
while (it != simiIt->second.end())
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
notify.insert(p);
++it;
++iAccepted;
}
else
{
it = simiIt->second.erase(it);
}
}
}
if (auto historyIt = subAccountHistory_.find(affectedAccount);
historyIt != subAccountHistory_.end())
{
auto& subs = historyIt->second;
auto it = subs.begin();
while (it != subs.end())
{
SubAccountHistoryInfoWeak const& info = it->second;
if (currLedgerSeq <= info.index->separationLedgerSeq)
{
++it;
continue;
}
if (auto isSptr = info.sinkWptr.lock(); isSptr)
{
accountHistoryNotify.emplace_back(
SubAccountHistoryInfo{.sink = isSptr, .index = info.index});
++it;
}
else
{
it = subs.erase(it);
}
}
if (subs.empty())
subAccountHistory_.erase(historyIt);
}
}
}
}
JLOG(journal_.trace()) << "pubAccountTransaction: "
<< "proposed=" << iProposed << ", accepted=" << iAccepted;
if (!notify.empty() || !accountHistoryNotify.empty())
{
auto const& stTxn = transaction.getTxn();
// Create two different Json objects, for different API versions
auto const metaRef = std::ref(transaction.getMeta());
auto const trResult = transaction.getResult();
MultiApiJson jvObj = transJson(stTxn, trResult, true, ledger, metaRef);
for (InfoSub::ref isrListener : notify)
{
jvObj.visit(
isrListener->getApiVersion(), //
[&](json::Value const& jv) { isrListener->send(jv, true); });
}
if (last)
jvObj.set(jss::account_history_boundary, true);
XRPL_ASSERT(
jvObj.isMember(jss::account_history_tx_stream) == MultiApiJson::IsMemberResult::None,
"xrpl::NetworkOPsImp::pubAccountTransaction : "
"account_history_tx_stream not set");
for (auto& info : accountHistoryNotify)
{
auto& index = info.index;
if (index->forwardTxIndex == 0 && !index->haveHistorical)
jvObj.set(jss::account_history_tx_first, true);
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
jvObj.visit(
info.sink->getApiVersion(), //
[&](json::Value const& jv) { info.sink->send(jv, true); });
}
}
}
void
NetworkOPsImp::pubProposedAccountTransaction(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const& tx,
TER result)
{
hash_set<InfoSub::pointer> notify;
int iProposed = 0;
std::vector<SubAccountHistoryInfo> accountHistoryNotify;
{
std::scoped_lock const sl(subLock_);
if (subRTAccount_.empty())
return;
if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty())
{
for (auto const& affectedAccount : tx->getMentionedAccounts())
{
if (auto simiIt = subRTAccount_.find(affectedAccount);
simiIt != subRTAccount_.end())
{
auto it = simiIt->second.begin();
while (it != simiIt->second.end())
{
InfoSub::pointer const p = it->second.lock();
if (p)
{
notify.insert(p);
++it;
++iProposed;
}
else
{
it = simiIt->second.erase(it);
}
}
}
}
}
}
JLOG(journal_.trace()) << "pubProposedAccountTransaction: " << iProposed;
if (!notify.empty() || !accountHistoryNotify.empty())
{
// Create two different Json objects, for different API versions
MultiApiJson jvObj = transJson(tx, result, false, ledger, std::nullopt);
for (InfoSub::ref isrListener : notify)
{
jvObj.visit(
isrListener->getApiVersion(), //
[&](json::Value const& jv) { isrListener->send(jv, true); });
}
XRPL_ASSERT(
jvObj.isMember(jss::account_history_tx_stream) == MultiApiJson::IsMemberResult::None,
"xrpl::NetworkOPs::pubProposedAccountTransaction : "
"account_history_tx_stream not set");
for (auto& info : accountHistoryNotify)
{
auto& index = info.index;
if (index->forwardTxIndex == 0 && !index->haveHistorical)
jvObj.set(jss::account_history_tx_first, true);
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
jvObj.visit(
info.sink->getApiVersion(), //
[&](json::Value const& jv) { info.sink->send(jv, true); });
}
}
}
//
// Monitoring
//
void
NetworkOPsImp::subAccount(
InfoSub::ref isrListener,
hash_set<AccountID> const& vnaAccountIDs,
bool rt)
{
SubInfoMapType& subMap = rt ? subRTAccount_ : subAccount_;
for (auto const& naAccountID : vnaAccountIDs)
{
JLOG(journal_.trace()) << "subAccount: account: " << toBase58(naAccountID);
isrListener->insertSubAccountInfo(naAccountID, rt);
}
std::scoped_lock const sl(subLock_);
for (auto const& naAccountID : vnaAccountIDs)
{
auto simIterator = subMap.find(naAccountID);
if (simIterator == subMap.end())
{
// Not found, note that account has a new single listener.
SubMapType usisElement;
usisElement[isrListener->getSeq()] = isrListener;
// VFALCO NOTE This is making a needless copy of naAccountID
subMap.insert(simIterator, make_pair(naAccountID, usisElement));
}
else
{
// Found, note that the account has another listener.
simIterator->second[isrListener->getSeq()] = isrListener;
}
}
}
void
NetworkOPsImp::unsubAccount(
InfoSub::ref isrListener,
hash_set<AccountID> const& vnaAccountIDs,
bool rt)
{
for (auto const& naAccountID : vnaAccountIDs)
{
// Remove from the InfoSub
isrListener->deleteSubAccountInfo(naAccountID, rt);
}
// Remove from the server
unsubAccountInternal(isrListener->getSeq(), vnaAccountIDs, rt);
}
void
NetworkOPsImp::unsubAccountInternal(
std::uint64_t uSeq,
hash_set<AccountID> const& vnaAccountIDs,
bool rt)
{
std::scoped_lock const sl(subLock_);
SubInfoMapType& subMap = rt ? subRTAccount_ : subAccount_;
for (auto const& naAccountID : vnaAccountIDs)
{
auto simIterator = subMap.find(naAccountID);
if (simIterator != subMap.end())
{
// Found
simIterator->second.erase(uSeq);
if (simIterator->second.empty())
{
// Don't need hash entry.
subMap.erase(simIterator);
}
}
}
}
void
NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
{
registry_.get().getJobQueue().addJob(JtClientAcctHist, "HistTxStream", [this, subInfo]() {
auto const& accountId = subInfo.index->accountId;
auto& lastLedgerSeq = subInfo.index->historyLastLedgerSeq;
auto& txHistoryIndex = subInfo.index->historyTxIndex;
JLOG(journal_.trace()) << "AccountHistory job for account " << toBase58(accountId)
<< " started. lastLedgerSeq=" << lastLedgerSeq;
auto isFirstTx = [&](std::shared_ptr<Transaction> const& tx,
std::shared_ptr<TxMeta> const& meta) -> bool {
/*
* genesis account: first tx is the one with seq 1
* other account: first tx is the one created the account
*/
if (accountId == kGenesisAccountId)
{
auto stx = tx->getSTransaction();
if (stx->getAccountID(sfAccount) == accountId && stx->getSeqValue() == 1)
return true;
}
return std::ranges::any_of(meta->getNodes(), [&](auto& node) {
if (node.getFieldU16(sfLedgerEntryType) != ltACCOUNT_ROOT)
return false;
if (node.isFieldPresent(sfNewFields))
{
if (auto inner = dynamic_cast<STObject const*>(node.peekAtPField(sfNewFields));
inner)
{
if (inner->isFieldPresent(sfAccount) &&
inner->getAccountID(sfAccount) == accountId)
{
return true;
}
}
}
return false;
});
};
auto send = [&](json::Value const& jvObj, bool unsubscribe) -> bool {
if (auto sptr = subInfo.sinkWptr.lock())
{
sptr->send(jvObj, true);
if (unsubscribe)
unsubAccountHistory(sptr, accountId, false);
return true;
}
return false;
};
auto sendMultiApiJson = [&](MultiApiJson const& jvObj, bool unsubscribe) -> bool {
if (auto sptr = subInfo.sinkWptr.lock())
{
jvObj.visit(
sptr->getApiVersion(), //
[&](json::Value const& jv) { sptr->send(jv, true); });
if (unsubscribe)
unsubAccountHistory(sptr, accountId, false);
return true;
}
return false;
};
auto getMoreTxns = [&](std::uint32_t minLedger,
std::uint32_t maxLedger,
std::optional<RelationalDatabase::AccountTxMarker> marker)
-> std::pair<
RelationalDatabase::AccountTxs,
std::optional<RelationalDatabase::AccountTxMarker>> {
auto& db = registry_.get().getRelationalDatabase();
RelationalDatabase::AccountTxPageOptions const options{
.account = accountId,
.ledgerRange = {.min = minLedger, .max = maxLedger},
.marker = marker,
.limit = 0,
.bAdmin = true,
.delegate = std::nullopt};
return db.newestAccountTxPage(options);
};
/*
* search backward until the genesis ledger or asked to stop
*/
while (lastLedgerSeq >= 2 && !subInfo.index->stopHistorical)
{
int feeChargeCount = 0;
if (auto sptr = subInfo.sinkWptr.lock(); sptr)
{
sptr->getConsumer().charge(Resource::kFeeMediumBurdenRpc);
++feeChargeCount;
}
else
{
JLOG(journal_.trace())
<< "AccountHistory job for account " << toBase58(accountId)
<< " no InfoSub. Fee charged " << feeChargeCount << " times.";
return;
}
// try to search in 1024 ledgers till reaching genesis ledgers
auto startLedgerSeq = (lastLedgerSeq > 1024 + 2 ? lastLedgerSeq - 1024 : 2);
JLOG(journal_.trace())
<< "AccountHistory job for account " << toBase58(accountId)
<< ", working on ledger range [" << startLedgerSeq << "," << lastLedgerSeq << "]";
auto haveRange = [&]() -> bool {
std::uint32_t validatedMin = UINT_MAX;
std::uint32_t validatedMax = 0;
auto haveSomeValidatedLedgers =
registry_.get().getLedgerMaster().getValidatedRange(validatedMin, validatedMax);
return haveSomeValidatedLedgers && validatedMin <= startLedgerSeq &&
lastLedgerSeq <= validatedMax;
}();
if (!haveRange)
{
JLOG(journal_.debug()) << "AccountHistory reschedule job for account "
<< toBase58(accountId) << ", incomplete ledger range ["
<< startLedgerSeq << "," << lastLedgerSeq << "]";
setAccountHistoryJobTimer(subInfo);
return;
}
std::optional<RelationalDatabase::AccountTxMarker> marker{};
while (!subInfo.index->stopHistorical)
{
auto dbResult = getMoreTxns(startLedgerSeq, lastLedgerSeq, marker);
auto const& txns = dbResult.first;
marker = dbResult.second;
size_t const numTxns = txns.size();
for (size_t i = 0; i < numTxns; ++i)
{
auto const& [tx, meta] = txns[i];
if (!tx || !meta)
{
JLOG(journal_.debug()) << "AccountHistory job for account "
<< toBase58(accountId) << " empty tx or meta.";
send(rpcError(RpcInternal), true);
return;
}
auto curTxLedger =
registry_.get().getLedgerMaster().getLedgerBySeq(tx->getLedger());
if (!curTxLedger)
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::NetworkOPsImp::addAccountHistoryJob : "
"getLedgerBySeq failed");
JLOG(journal_.debug()) << "AccountHistory job for account "
<< toBase58(accountId) << " no ledger.";
send(rpcError(RpcInternal), true);
return;
// LCOV_EXCL_STOP
}
std::shared_ptr<STTx const> const stTxn = tx->getSTransaction();
if (!stTxn)
{
// LCOV_EXCL_START
UNREACHABLE(
"NetworkOPsImp::addAccountHistoryJob : "
"getSTransaction failed");
JLOG(journal_.debug()) << "AccountHistory job for account "
<< toBase58(accountId) << " getSTransaction failed.";
send(rpcError(RpcInternal), true);
return;
// LCOV_EXCL_STOP
}
auto const ref = std::ref(*meta);
auto const trR = meta->getResultTER();
MultiApiJson jvTx = transJson(stTxn, trR, true, curTxLedger, ref);
jvTx.set(jss::account_history_tx_index, txHistoryIndex--);
if (i + 1 == numTxns || txns[i + 1].first->getLedger() != tx->getLedger())
jvTx.set(jss::account_history_boundary, true);
if (isFirstTx(tx, meta))
{
jvTx.set(jss::account_history_tx_first, true);
sendMultiApiJson(jvTx, false);
JLOG(journal_.trace()) << "AccountHistory job for account "
<< toBase58(accountId) << " done, found last tx.";
return;
}
sendMultiApiJson(jvTx, false);
}
if (marker)
{
JLOG(journal_.trace())
<< "AccountHistory job for account " << toBase58(accountId)
<< " paging, marker=" << marker->ledgerSeq << ":" << marker->txnSeq;
}
else
{
break;
}
}
if (!subInfo.index->stopHistorical)
{
lastLedgerSeq = startLedgerSeq - 1;
if (lastLedgerSeq <= 1)
{
JLOG(journal_.trace())
<< "AccountHistory job for account " << toBase58(accountId)
<< " done, reached genesis ledger.";
return;
}
}
}
});
}
void
NetworkOPsImp::subAccountHistoryStart(
std::shared_ptr<ReadView const> const& ledger,
SubAccountHistoryInfoWeak& subInfo)
{
subInfo.index->separationLedgerSeq = ledger->seq();
auto const& accountId = subInfo.index->accountId;
auto const accountKeylet = keylet::account(accountId);
if (!ledger->exists(accountKeylet))
{
JLOG(journal_.debug()) << "subAccountHistoryStart, no account " << toBase58(accountId)
<< ", no need to add AccountHistory job.";
return;
}
if (accountId == kGenesisAccountId)
{
if (auto const sleAcct = ledger->read(accountKeylet); sleAcct)
{
if (sleAcct->getFieldU32(sfSequence) == 1)
{
JLOG(journal_.debug())
<< "subAccountHistoryStart, genesis account " << toBase58(accountId)
<< " does not have tx, no need to add AccountHistory job.";
return;
}
}
else
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::NetworkOPsImp::subAccountHistoryStart : failed to "
"access genesis account");
return;
// LCOV_EXCL_STOP
}
}
subInfo.index->historyLastLedgerSeq = ledger->seq();
subInfo.index->haveHistorical = true;
JLOG(journal_.debug()) << "subAccountHistoryStart, add AccountHistory job: accountId="
<< toBase58(accountId) << ", currentLedgerSeq=" << ledger->seq();
addAccountHistoryJob(subInfo);
}
ErrorCodeI
NetworkOPsImp::subAccountHistory(InfoSub::ref isrListener, AccountID const& accountId)
{
if (!isrListener->insertSubAccountHistory(accountId))
{
JLOG(journal_.debug()) << "subAccountHistory, already subscribed to account "
<< toBase58(accountId);
return RpcInvalidParams;
}
std::scoped_lock const sl(subLock_);
SubAccountHistoryInfoWeak ahi{
.sinkWptr = isrListener, .index = std::make_shared<SubAccountHistoryIndex>(accountId)};
auto simIterator = subAccountHistory_.find(accountId);
if (simIterator == subAccountHistory_.end())
{
hash_map<std::uint64_t, SubAccountHistoryInfoWeak> inner;
inner.emplace(isrListener->getSeq(), ahi);
subAccountHistory_.insert(simIterator, std::make_pair(accountId, inner));
}
else
{
simIterator->second.emplace(isrListener->getSeq(), ahi);
}
auto const ledger = registry_.get().getLedgerMaster().getValidatedLedger();
if (ledger)
{
subAccountHistoryStart(ledger, ahi);
}
else
{
// The node does not have validated ledgers, so wait for
// one before start streaming.
// In this case, the subscription is also considered successful.
JLOG(journal_.debug()) << "subAccountHistory, no validated ledger yet, delay start";
}
return RpcSuccess;
}
void
NetworkOPsImp::unsubAccountHistory(
InfoSub::ref isrListener,
AccountID const& account,
bool historyOnly)
{
if (!historyOnly)
isrListener->deleteSubAccountHistory(account);
unsubAccountHistoryInternal(isrListener->getSeq(), account, historyOnly);
}
void
NetworkOPsImp::unsubAccountHistoryInternal(
std::uint64_t seq,
AccountID const& account,
bool historyOnly)
{
std::scoped_lock const sl(subLock_);
auto simIterator = subAccountHistory_.find(account);
if (simIterator != subAccountHistory_.end())
{
auto& subInfoMap = simIterator->second;
auto subInfoIter = subInfoMap.find(seq);
if (subInfoIter != subInfoMap.end())
{
subInfoIter->second.index->stopHistorical = true;
}
if (!historyOnly)
{
simIterator->second.erase(seq);
if (simIterator->second.empty())
{
subAccountHistory_.erase(simIterator);
}
}
JLOG(journal_.debug()) << "unsubAccountHistory, account " << toBase58(account)
<< ", historyOnly = " << (historyOnly ? "true" : "false");
}
}
bool
NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book)
{
// Server-side insert first, then InfoSub bookkeeping. If the InfoSub-side
// insert throws, the orphan in subBook_ is cleared by the expired-weak_ptr
// prune in pubBookTransaction. With the reverse ordering, ~InfoSub would
// call unsubBookInternal for a key that was never inserted server-side.
{
std::scoped_lock const sl(subLock_);
subBook_[book].try_emplace(isrListener->getSeq(), isrListener);
}
isrListener->insertBookSubscription(book);
return true;
}
bool
NetworkOPsImp::unsubBook(InfoSub::ref isrListener, Book const& book)
{
// Mirrors unsubAccount: clear the per-subscriber tracking set first so
// ~InfoSub does not re-issue an unsubBookInternal for a book the caller
// already removed, then erase the server-side entry.
isrListener->deleteBookSubscription(book);
return unsubBookInternal(isrListener->getSeq(), book);
}
bool
NetworkOPsImp::unsubBookInternal(std::uint64_t uSeq, Book const& book)
{
std::scoped_lock const sl(subLock_);
auto it = subBook_.find(book);
if (it == subBook_.end())
return false;
bool const erased = it->second.erase(uSeq) != 0u;
if (it->second.empty())
subBook_.erase(it);
return erased;
}
std::uint32_t
NetworkOPsImp::acceptLedger(std::optional<std::chrono::milliseconds> consensusDelay)
{
// This code-path is exclusively used when the server is in standalone
// mode via `ledger_accept`
XRPL_ASSERT(standalone_, "xrpl::NetworkOPsImp::acceptLedger : is standalone");
if (!standalone_)
Throw<std::runtime_error>("Operation only possible in STANDALONE mode.");
// FIXME Could we improve on this and remove the need for a specialized
// API in Consensus?
beginConsensus(ledgerMaster_.getClosedLedger()->header().hash, {});
consensus_.simulate(registry_.get().getTimeKeeper().closeTime(), consensusDelay);
return ledgerMaster_.getCurrentLedger()->header().seq;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subLedger(InfoSub::ref isrListener, json::Value& jvResult)
{
if (auto lpClosed = ledgerMaster_.getValidatedLedger())
{
jvResult[jss::ledger_index] = lpClosed->header().seq;
jvResult[jss::ledger_hash] = to_string(lpClosed->header().hash);
jvResult[jss::ledger_time] =
json::Value::UInt(lpClosed->header().closeTime.time_since_epoch().count());
if (!lpClosed->rules().enabled(featureXRPFees))
jvResult[jss::fee_ref] = kFeeUnitsDeprecated;
jvResult[jss::fee_base] = lpClosed->fees().base.jsonClipped();
jvResult[jss::reserve_base] = lpClosed->fees().reserve.jsonClipped();
jvResult[jss::reserve_inc] = lpClosed->fees().increment.jsonClipped();
jvResult[jss::network_id] = registry_.get().getNetworkIDService().getNetworkID();
}
if ((mode_ >= OperatingMode::SYNCING) && !isNeedNetworkLedger())
{
jvResult[jss::validated_ledgers] = registry_.get().getLedgerMaster().getCompleteLedgers();
}
std::scoped_lock const sl(subLock_);
return streamMaps_[SLedger].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subBookChanges(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SBookChanges].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubLedger(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SLedger].erase(uSeq) != 0u;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubBookChanges(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SBookChanges].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subManifests(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SManifests].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubManifests(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SManifests].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subServer(InfoSub::ref isrListener, json::Value& jvResult, bool admin)
{
uint256 uRandom;
if (standalone_)
jvResult[jss::stand_alone] = standalone_;
// CHECKME: is it necessary to provide a random number here?
beast::rngfill(uRandom.begin(), uRandom.size(), cryptoPrng());
auto const& feeTrack = registry_.get().getFeeTrack();
jvResult[jss::random] = to_string(uRandom);
jvResult[jss::server_status] = strOperatingMode(admin);
jvResult[jss::load_base] = feeTrack.getLoadBase();
jvResult[jss::load_factor] = feeTrack.getLoadFactor();
jvResult[jss::hostid] = getHostId(admin);
jvResult[jss::pubkey_node] =
toBase58(TokenType::NodePublic, registry_.get().getApp().nodeIdentity().first);
std::scoped_lock const sl(subLock_);
return streamMaps_[SServer].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubServer(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SServer].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subTransactions(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[STransactions].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubTransactions(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[STransactions].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subRTTransactions(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SRtTransactions].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubRTTransactions(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SRtTransactions].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subValidations(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SValidations].emplace(isrListener->getSeq(), isrListener).second;
}
void
NetworkOPsImp::stateAccounting(json::Value& obj)
{
accounting_.json(obj);
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubValidations(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SValidations].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subPeerStatus(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SPeerStatus].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubPeerStatus(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SPeerStatus].erase(uSeq) != 0u;
}
// <-- bool: true=added, false=already there
bool
NetworkOPsImp::subConsensus(InfoSub::ref isrListener)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SConsensusPhase].emplace(isrListener->getSeq(), isrListener).second;
}
// <-- bool: true=erased, false=was not there
bool
NetworkOPsImp::unsubConsensus(std::uint64_t uSeq)
{
std::scoped_lock const sl(subLock_);
return streamMaps_[SConsensusPhase].erase(uSeq) != 0u;
}
InfoSub::pointer
NetworkOPsImp::findRpcSub(std::string const& strUrl)
{
std::scoped_lock const sl(subLock_);
auto const it = rpcSubMap_.find(strUrl);
if (it != rpcSubMap_.end())
return it->second;
return InfoSub::pointer();
}
InfoSub::pointer
NetworkOPsImp::addRpcSub(std::string const& strUrl, InfoSub::ref rspEntry)
{
std::scoped_lock const sl(subLock_);
rpcSubMap_.emplace(strUrl, rspEntry);
return rspEntry;
}
bool
NetworkOPsImp::tryRemoveRpcSub(std::string const& strUrl)
{
std::scoped_lock const sl(subLock_);
auto pInfo = findRpcSub(strUrl);
if (!pInfo)
return false;
// check to see if any of the stream maps still hold a weak reference to
// this entry before removing
for (SubMapType const& map : streamMaps_)
{
if (map.contains(pInfo->getSeq()))
return false;
}
rpcSubMap_.erase(strUrl);
return true;
}
#ifndef USE_NEW_BOOK_PAGE
// NIKB FIXME this should be looked at. There's no reason why this shouldn't
// work, but it demonstrated poor performance.
//
void
NetworkOPsImp::getBookPage(
std::shared_ptr<ReadView const>& lpLedger,
Book const& book,
AccountID const& uTakerID,
bool const bProof,
unsigned int iLimit,
json::Value const& jvMarker,
json::Value& jvResult)
{ // CAUTION: This is the old get book page logic
json::Value& jvOffers = (jvResult[jss::offers] = json::Value(json::ValueType::Array));
std::unordered_map<AccountID, STAmount> umBalance;
uint256 const uBookBase = getBookBase(book);
uint256 const uBookEnd = getQualityNext(uBookBase);
uint256 uTipIndex = uBookBase;
if (auto stream = journal_.trace())
{
stream << "getBookPage:" << book;
stream << "getBookPage: uBookBase=" << uBookBase;
stream << "getBookPage: uBookEnd=" << uBookEnd;
stream << "getBookPage: uTipIndex=" << uTipIndex;
}
ReadView const& view = *lpLedger;
bool const bGlobalFreeze =
isGlobalFrozen(view, book.out.getIssuer()) || isGlobalFrozen(view, book.in.getIssuer());
bool bDone = false;
bool bDirectAdvance = true;
SLE::const_pointer sleOfferDir;
uint256 offerIndex;
unsigned int uBookEntry = 0;
STAmount saDirRate;
auto const rate = transferRate(view, book.out.getIssuer());
auto viewJ = registry_.get().getJournal("View");
while (!bDone && iLimit-- > 0)
{
if (bDirectAdvance)
{
bDirectAdvance = false;
JLOG(journal_.trace()) << "getBookPage: bDirectAdvance";
auto const ledgerIndex = view.succ(uTipIndex, uBookEnd);
if (ledgerIndex)
{
sleOfferDir = view.read(keylet::page(*ledgerIndex));
}
else
{
sleOfferDir.reset();
}
if (!sleOfferDir)
{
JLOG(journal_.trace()) << "getBookPage: bDone";
bDone = true;
}
else
{
uTipIndex = sleOfferDir->key();
saDirRate = amountFromQuality(getQuality(uTipIndex));
cdirFirst(view, uTipIndex, sleOfferDir, uBookEntry, offerIndex);
JLOG(journal_.trace()) << "getBookPage: uTipIndex=" << uTipIndex;
JLOG(journal_.trace()) << "getBookPage: offerIndex=" << offerIndex;
}
}
if (!bDone)
{
auto sleOffer = view.read(keylet::offer(offerIndex));
if (sleOffer)
{
auto const uOfferOwnerID = sleOffer->getAccountID(sfAccount);
auto const& saTakerGets = sleOffer->getFieldAmount(sfTakerGets);
auto const& saTakerPays = sleOffer->getFieldAmount(sfTakerPays);
STAmount saOwnerFunds;
bool firstOwnerOffer(true);
if (book.out.getIssuer() == uOfferOwnerID)
{
// If an offer is selling issuer's own IOUs, it is fully
// funded.
saOwnerFunds = saTakerGets;
}
else if (bGlobalFreeze)
{
// If either asset is globally frozen, consider all offers
// that aren't ours to be totally unfunded
saOwnerFunds.clear(book.out);
}
else
{
auto umBalanceEntry = umBalance.find(uOfferOwnerID);
if (umBalanceEntry != umBalance.end())
{
// Found in running balance table.
saOwnerFunds = umBalanceEntry->second;
firstOwnerOffer = false;
}
else
{
// Did not find balance in table.
saOwnerFunds = accountHolds(
view,
uOfferOwnerID,
book.out,
FreezeHandling::ZeroIfFrozen,
AuthHandling::ZeroIfUnauthorized,
viewJ);
if (saOwnerFunds < beast::kZero)
{
// Treat negative funds as zero.
saOwnerFunds.clear();
}
}
}
json::Value jvOffer = sleOffer->getJson(JsonOptions::Values::None);
STAmount saTakerGetsFunded;
STAmount saOwnerFundsLimit = saOwnerFunds;
Rate offerRate = kParityRate;
if (rate != kParityRate
// Have a transfer fee.
&& uTakerID != book.out.getIssuer()
// Not taking offers of own IOUs.
&& book.out.getIssuer() != uOfferOwnerID)
// Offer owner not issuing ownfunds
{
// Need to charge a transfer fee to offer owner.
offerRate = rate;
saOwnerFundsLimit = divide(saOwnerFunds, offerRate);
}
if (saOwnerFundsLimit >= saTakerGets)
{
// Sufficient funds no shenanigans.
saTakerGetsFunded = saTakerGets;
}
else
{
// Only provide, if not fully funded.
saTakerGetsFunded = saOwnerFundsLimit;
saTakerGetsFunded.setJson(jvOffer[jss::taker_gets_funded]);
std::min(
saTakerPays, multiply(saTakerGetsFunded, saDirRate, saTakerPays.asset()))
.setJson(jvOffer[jss::taker_pays_funded]);
}
STAmount const saOwnerPays = (kParityRate == offerRate)
? saTakerGetsFunded
: std::min(saOwnerFunds, multiply(saTakerGetsFunded, offerRate));
umBalance[uOfferOwnerID] = saOwnerFunds - saOwnerPays;
// Include all offers funded and unfunded
json::Value& jvOf = jvOffers.append(jvOffer);
jvOf[jss::quality] = saDirRate.getText();
if (firstOwnerOffer)
jvOf[jss::owner_funds] = saOwnerFunds.getText();
}
else
{
JLOG(journal_.warn()) << "Missing offer";
}
if (!cdirNext(view, uTipIndex, sleOfferDir, uBookEntry, offerIndex))
{
bDirectAdvance = true;
}
else
{
JLOG(journal_.trace()) << "getBookPage: offerIndex=" << offerIndex;
}
}
}
// jvResult[jss::marker] = json::Value(json::ValueType::Array);
// jvResult[jss::nodes] = json::Value(json::ValueType::Array);
}
#else
// This is the new code that uses the book iterators
// It has temporarily been disabled
void
NetworkOPsImp::getBookPage(
std::shared_ptr<ReadView const> lpLedger,
Book const& book,
AccountID const& uTakerID,
bool const bProof,
unsigned int iLimit,
json::Value const& jvMarker,
json::Value& jvResult)
{
auto& jvOffers = (jvResult[jss::offers] = json::Value(json::ValueType::Array));
std::map<AccountID, STAmount> umBalance;
MetaView lesActive(lpLedger, tapNONE, true);
OrderBookIterator obIterator(lesActive, book);
auto const rate = transferRate(lesActive, book.out.account);
bool const bGlobalFreeze =
lesActive.isGlobalFrozen(book.out.account) || lesActive.isGlobalFrozen(book.in.account);
while (iLimit-- > 0 && obIterator.nextOffer())
{
SLE::pointer sleOffer = obIterator.getCurrentOffer();
if (sleOffer)
{
auto const uOfferOwnerID = sleOffer->getAccountID(sfAccount);
auto const& saTakerGets = sleOffer->getFieldAmount(sfTakerGets);
auto const& saTakerPays = sleOffer->getFieldAmount(sfTakerPays);
STAmount saDirRate = obIterator.getCurrentRate();
STAmount saOwnerFunds;
if (book.out.account == uOfferOwnerID)
{
// If offer is selling issuer's own IOUs, it is fully funded.
saOwnerFunds = saTakerGets;
}
else if (bGlobalFreeze)
{
// If either asset is globally frozen, consider all offers
// that aren't ours to be totally unfunded
saOwnerFunds.clear(book.out);
}
else
{
auto umBalanceEntry = umBalance.find(uOfferOwnerID);
if (umBalanceEntry != umBalance.end())
{
// Found in running balance table.
saOwnerFunds = umBalanceEntry->second;
}
else
{
// Did not find balance in table.
saOwnerFunds = lesActive.accountHolds(
uOfferOwnerID,
book.out.currency,
book.out.account,
FreezeHandling::ZeroIfFrozen);
if (saOwnerFunds.isNegative())
{
// Treat negative funds as zero.
saOwnerFunds.zero();
}
}
}
json::Value jvOffer = sleOffer->getJson(JsonOptions::Values::None);
STAmount saTakerGetsFunded;
STAmount saOwnerFundsLimit = saOwnerFunds;
Rate offerRate = parityRate;
if (rate != parityRate
// Have a transfer fee.
&& uTakerID != book.out.account
// Not taking offers of own IOUs.
&& book.out.account != uOfferOwnerID)
// Offer owner not issuing ownfunds
{
// Need to charge a transfer fee to offer owner.
offerRate = rate;
saOwnerFundsLimit = divide(saOwnerFunds, offerRate);
}
if (saOwnerFundsLimit >= saTakerGets)
{
// Sufficient funds no shenanigans.
saTakerGetsFunded = saTakerGets;
}
else
{
// Only provide, if not fully funded.
saTakerGetsFunded = saOwnerFundsLimit;
saTakerGetsFunded.setJson(jvOffer[jss::taker_gets_funded]);
// TODO(tom): The result of this expression is not used - what's
// going on here?
std::min(saTakerPays, multiply(saTakerGetsFunded, saDirRate, saTakerPays.asset()))
.setJson(jvOffer[jss::taker_pays_funded]);
}
STAmount saOwnerPays = (parityRate == offerRate)
? saTakerGetsFunded
: std::min(saOwnerFunds, multiply(saTakerGetsFunded, offerRate));
umBalance[uOfferOwnerID] = saOwnerFunds - saOwnerPays;
if (!saOwnerFunds.isZero() || uOfferOwnerID == uTakerID)
{
// Only provide funded offers and offers of the taker.
json::Value& jvOf = jvOffers.append(jvOffer);
jvOf[jss::quality] = saDirRate.getText();
}
}
}
// jvResult[jss::marker] = json::Value(json::ValueType::Array);
// jvResult[jss::nodes] = json::Value(json::ValueType::Array);
}
#endif
inline void
NetworkOPsImp::collectMetrics()
{
auto [counters, mode, start, initialSync] = accounting_.getCounterData();
auto const current = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start);
counters[static_cast<std::size_t>(mode)].dur += current;
std::scoped_lock const lock(statsMutex_);
stats_.disconnectedDuration.set(
counters[static_cast<std::size_t>(OperatingMode::DISCONNECTED)].dur.count());
stats_.connectedDuration.set(
counters[static_cast<std::size_t>(OperatingMode::CONNECTED)].dur.count());
stats_.syncingDuration.set(
counters[static_cast<std::size_t>(OperatingMode::SYNCING)].dur.count());
stats_.trackingDuration.set(
counters[static_cast<std::size_t>(OperatingMode::TRACKING)].dur.count());
stats_.fullDuration.set(counters[static_cast<std::size_t>(OperatingMode::FULL)].dur.count());
stats_.disconnectedTransitions.set(
counters[static_cast<std::size_t>(OperatingMode::DISCONNECTED)].transitions);
stats_.connectedTransitions.set(
counters[static_cast<std::size_t>(OperatingMode::CONNECTED)].transitions);
stats_.syncingTransitions.set(
counters[static_cast<std::size_t>(OperatingMode::SYNCING)].transitions);
stats_.trackingTransitions.set(
counters[static_cast<std::size_t>(OperatingMode::TRACKING)].transitions);
stats_.fullTransitions.set(counters[static_cast<std::size_t>(OperatingMode::FULL)].transitions);
}
void
NetworkOPsImp::StateAccounting::mode(OperatingMode om)
{
auto now = std::chrono::steady_clock::now();
std::scoped_lock const lock(mutex_);
++counters_[static_cast<std::size_t>(om)].transitions;
if (om == OperatingMode::FULL && counters_[static_cast<std::size_t>(om)].transitions == 1)
{
initialSyncUs_ =
std::chrono::duration_cast<std::chrono::microseconds>(now - processStart_).count();
}
counters_[static_cast<std::size_t>(mode_)].dur +=
std::chrono::duration_cast<std::chrono::microseconds>(now - start_);
mode_ = om;
start_ = now;
}
void
NetworkOPsImp::StateAccounting::json(json::Value& obj) const
{
auto [counters, mode, start, initialSync] = getCounterData();
auto const current = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start);
counters[static_cast<std::size_t>(mode)].dur += current;
obj[jss::state_accounting] = json::ValueType::Object;
for (auto i = static_cast<std::size_t>(OperatingMode::DISCONNECTED);
i <= static_cast<std::size_t>(OperatingMode::FULL);
++i)
{
obj[jss::state_accounting][kStates[i]] = json::ValueType::Object;
auto& state = obj[jss::state_accounting][kStates[i]];
state[jss::transitions] = std::to_string(counters[i].transitions);
state[jss::duration_us] = std::to_string(counters[i].dur.count());
}
obj[jss::server_state_duration_us] = std::to_string(current.count());
if (initialSync != 0u)
obj[jss::initial_sync_duration_us] = std::to_string(initialSync);
}
//------------------------------------------------------------------------------
std::unique_ptr<NetworkOPs>
makeNetworkOPs(
ServiceRegistry& registry,
NetworkOPs::clock_type& clock,
bool standalone,
std::size_t minPeerCount,
bool startValid,
JobQueue& jobQueue,
LedgerMaster& ledgerMaster,
ValidatorKeys const& validatorKeys,
boost::asio::io_context& ioCtx,
beast::Journal journal,
beast::insight::Collector::ptr const& collector)
{
return std::make_unique<NetworkOPsImp>(
registry,
clock,
standalone,
minPeerCount,
startValid,
jobQueue,
ledgerMaster,
validatorKeys,
ioCtx,
journal,
collector);
}
} // namespace xrpl