mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 19:57:09 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c543f817 | ||
|
|
dcfe881290 | ||
|
|
56bedcc958 | ||
|
|
c9455093b4 |
No files matched your search
@@ -848,6 +848,68 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [lite_mode]
|
||||
#
|
||||
# Run as a lite node: follow the network's validated ledgers without
|
||||
# holding their state. For each validated ledger the server fetches the
|
||||
# header, the whole transaction map (small, and what account subscriptions
|
||||
# and `tx` answer from) and only the state nodes on the paths from the root
|
||||
# to the objects in [lite_watch], and to the objects the transactions in
|
||||
# that ledger created or touched for the watched accounts. Everything else
|
||||
# stays on the network. Sync takes a few request/response rounds, not
|
||||
# hours, and a ledger costs a few kilobytes rather than the full state.
|
||||
#
|
||||
# This is for small devices that need to see whether a transaction
|
||||
# succeeded, read a balance or a trust line, or submit a transaction, and
|
||||
# do not need to serve other nodes. In lite mode the server:
|
||||
#
|
||||
# - takes no part in consensus and never closes a ledger of its own;
|
||||
# - serves no ledger data to peers and advertises no ledger range;
|
||||
# - fetches no history: only ledgers validated after it started;
|
||||
# - implies [node_db] type=rwdb (nothing is written to a node store;
|
||||
# the last ledger_history ledgers are kept in memory), path_search_max
|
||||
# 0, and refuses [validation_seed] / [validator_token] and standalone;
|
||||
# - answers `ledger_index: current` and `closed` with the validated
|
||||
# ledger;
|
||||
# - relays a submitted transaction after checking its form and
|
||||
# signature, and reports terLITE_RELAYED instead of applying it: read
|
||||
# the outcome with `tx` or an account subscription;
|
||||
# - answers a request for any object it does not hold with
|
||||
# liteNotWatched. Add the object with the lite_watch RPC; it is read
|
||||
# from the next validated ledger.
|
||||
#
|
||||
# Legal values are 0 (the default) and 1.
|
||||
#
|
||||
# Suggested companions for a device with little memory:
|
||||
#
|
||||
# [node_size] tiny
|
||||
# [ledger_history] 64
|
||||
# [peers_max] 3
|
||||
#
|
||||
# [lite_watch]
|
||||
#
|
||||
# One entry per line: a base58 account address (its AccountRoot is watched,
|
||||
# and the objects its transactions create or touch are discovered as they
|
||||
# happen) or the 64 hex digit key of any ledger object. The fee settings,
|
||||
# amendments, skip list and negative UNL are always watched.
|
||||
#
|
||||
# Entries can be added and removed while running with the lite_watch and
|
||||
# lite_unwatch admin RPCs, which also take trust lines by their parties;
|
||||
# lite_status lists what is watched and why.
|
||||
#
|
||||
# [lite_discover]
|
||||
#
|
||||
# Whether a lite node grows its watch set from the transactions it sees:
|
||||
# an affected object naming a watched account is watched from then on, and
|
||||
# a transaction that touched anything watched has every object it created
|
||||
# watched (the trust line, escrow, check, offer, URIToken or hook state it
|
||||
# made for the account). Deleted objects are forgotten again.
|
||||
#
|
||||
# Legal values are 1 (the default) and 0, which watches only what is
|
||||
# configured.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [ledger_history]
|
||||
#
|
||||
# The number of past ledgers to acquire on server startup and the minimum to
|
||||
|
||||
@@ -157,7 +157,11 @@ enum error_code_i {
|
||||
// Simulate
|
||||
rpcTX_SIGNED = 97,
|
||||
|
||||
rpcLAST = rpcTX_SIGNED // rpcLAST should always equal the last code.
|
||||
// Lite mode: the object is not among the watched keys, so this node
|
||||
// does not hold it
|
||||
rpcLITE_NOT_WATCHED = 98,
|
||||
|
||||
rpcLAST = rpcLITE_NOT_WATCHED // rpcLAST should always equal the last code.
|
||||
};
|
||||
|
||||
/** Codes returned in the `warnings` array of certain RPC commands.
|
||||
|
||||
@@ -238,8 +238,11 @@ enum TERcodes : TERUnderlyingType {
|
||||
terQUEUED, // Transaction is being held in TxQ until fee drops
|
||||
terPRE_TICKET, // Ticket is not yet in ledger but might be on its way
|
||||
terNO_AMM, // AMM doesn't exist for the asset pair
|
||||
terNO_HOOK // Transaction requires a non-existent hook definition
|
||||
terNO_HOOK, // Transaction requires a non-existent hook definition
|
||||
// (referenced by sfHookHash)
|
||||
terLITE_RELAYED // Relayed by a [lite_mode] node that holds no state to
|
||||
// apply it against; the result is in the validated
|
||||
// ledger it turns up in, if any
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -113,7 +113,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
|
||||
{rpcLEDGER_MISSING, "ledgerMissing", "One or more ledgers in the specified range is missing", 406},
|
||||
{rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}};
|
||||
{rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{rpcLITE_NOT_WATCHED, "liteNotWatched", "This lite node does not watch that ledger object; add it with lite_watch.", 404}};
|
||||
// clang-format on
|
||||
|
||||
// Sort and validate unorderedErrorInfos at compile time. Should be
|
||||
|
||||
@@ -242,6 +242,7 @@ transResults()
|
||||
MAKE_ERROR(terPRE_TICKET, "Ticket is not yet in ledger."),
|
||||
MAKE_ERROR(terNO_HOOK, "No hook with that hash exists on the ledger."),
|
||||
MAKE_ERROR(terNO_AMM, "AMM doesn't exist for the asset pair."),
|
||||
MAKE_ERROR(terLITE_RELAYED, "Relayed to the network by a lite node; not applied locally."),
|
||||
|
||||
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
|
||||
MAKE_ERROR(tesPARTIAL, "The transaction was applied but should be submitted again until returning tesSUCCESS."),
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/shamap/common.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class LiteWatch_test : public beast::unit_test::suite
|
||||
{
|
||||
static Config
|
||||
liteConfig(std::vector<std::string> watch)
|
||||
{
|
||||
Config cfg;
|
||||
cfg.LITE_MODE = true;
|
||||
cfg.LITE_WATCH = std::move(watch);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void
|
||||
testConfigLines()
|
||||
{
|
||||
testcase("config lines");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
|
||||
std::string err;
|
||||
auto const acct = parseLiteWatchLine(" " + alice.human() + " ", err);
|
||||
BEAST_EXPECT(acct && acct->first && *acct->first == alice.id());
|
||||
BEAST_EXPECT(acct && acct->second == keylet::account(alice).key);
|
||||
|
||||
auto const key = keylet::fees().key;
|
||||
auto const raw = parseLiteWatchLine(to_string(key), err);
|
||||
BEAST_EXPECT(raw && !raw->first && raw->second == key);
|
||||
|
||||
BEAST_EXPECT(!parseLiteWatchLine("", err));
|
||||
BEAST_EXPECT(!parseLiteWatchLine("not a key", err));
|
||||
BEAST_EXPECT(!parseLiteWatchLine(to_string(key).substr(1), err));
|
||||
}
|
||||
|
||||
void
|
||||
testWatchSet()
|
||||
{
|
||||
testcase("watch set");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
Env env{*this};
|
||||
LiteWatch watch{liteConfig({alice.human()}), env.journal};
|
||||
|
||||
BEAST_EXPECT(watch.enabled());
|
||||
BEAST_EXPECT(watch.isWatchedAccount(alice));
|
||||
BEAST_EXPECT(watch.isWatched(keylet::account(alice).key));
|
||||
BEAST_EXPECT(!watch.isWatchedAccount(bob));
|
||||
|
||||
// The keys every ledger needs are there, and cannot be removed.
|
||||
for (auto const& k :
|
||||
{keylet::fees().key,
|
||||
keylet::amendments().key,
|
||||
keylet::skip().key,
|
||||
keylet::negativeUNL().key})
|
||||
{
|
||||
BEAST_EXPECT(watch.isWatched(k));
|
||||
BEAST_EXPECT(!watch.unwatchKey(k));
|
||||
BEAST_EXPECT(watch.isWatched(k));
|
||||
}
|
||||
BEAST_EXPECT(watch.isWatched(keylet::hook(alice).key));
|
||||
BEAST_EXPECT(watch.size() == 6);
|
||||
|
||||
BEAST_EXPECT(watch.watchAccount(bob));
|
||||
BEAST_EXPECT(!watch.watchAccount(bob));
|
||||
BEAST_EXPECT(watch.isWatched(keylet::account(bob).key));
|
||||
BEAST_EXPECT(watch.unwatchAccount(bob));
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::account(bob).key));
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::hook(bob).key));
|
||||
BEAST_EXPECT(!watch.unwatchAccount(bob));
|
||||
|
||||
auto const k = keylet::ownerDir(alice).key;
|
||||
BEAST_EXPECT(watch.watchKey(k, LiteWatch::Tier::discovered, "test"));
|
||||
BEAST_EXPECT(!watch.watchKey(k));
|
||||
// Asking for a discovered key by name promotes it: it now survives
|
||||
// the account it was discovered through being unwatched.
|
||||
BEAST_EXPECT(watch.unwatchAccount(alice));
|
||||
BEAST_EXPECT(watch.isWatched(k));
|
||||
BEAST_EXPECT(watch.unwatchKey(k));
|
||||
BEAST_EXPECT(!watch.isWatched(k));
|
||||
|
||||
auto const json = watch.getJson();
|
||||
BEAST_EXPECT(json[jss::enabled].asBool());
|
||||
BEAST_EXPECT(json["system"].asUInt() == 4);
|
||||
}
|
||||
|
||||
void
|
||||
testDiscover()
|
||||
{
|
||||
testcase("discover");
|
||||
|
||||
using namespace jtx;
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
Env env{*this};
|
||||
env.fund(XRP(10000), gw, alice, bob);
|
||||
env.close();
|
||||
|
||||
LiteWatch watch{liteConfig({alice.human()}), env.journal};
|
||||
auto const line = keylet::line(alice, gw, USD.currency).key;
|
||||
auto const bobLine = keylet::line(bob, gw, USD.currency).key;
|
||||
|
||||
// Nothing in the funding ledger concerns alice's objects beyond her
|
||||
// root, which is configured already.
|
||||
watch.discover(*env.closed());
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::account(bob).key));
|
||||
BEAST_EXPECT(!watch.isWatched(line));
|
||||
|
||||
env.trust(USD(100), alice);
|
||||
env.trust(USD(100), bob);
|
||||
env(pay(gw, alice, USD(50)));
|
||||
env.close();
|
||||
|
||||
auto const added = watch.discover(*env.closed());
|
||||
BEAST_EXPECT(added > 0);
|
||||
|
||||
// alice's new trust line names her as a party; bob's does not.
|
||||
BEAST_EXPECT(watch.isWatched(line));
|
||||
BEAST_EXPECT(!watch.isWatched(bobLine));
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::account(bob).key));
|
||||
// Her owner directory names her as its owner.
|
||||
BEAST_EXPECT(watch.isWatched(keylet::ownerDir(alice).key));
|
||||
|
||||
// Paying the line back to zero and clearing the limit deletes it,
|
||||
// and a discovered object that is deleted is forgotten.
|
||||
env(pay(alice, gw, USD(50)));
|
||||
env(trust(alice, USD(0)));
|
||||
env.close();
|
||||
watch.discover(*env.closed());
|
||||
BEAST_EXPECT(!watch.isWatched(line));
|
||||
BEAST_EXPECT(watch.isWatched(keylet::account(alice).key));
|
||||
|
||||
// With discovery off nothing is learned.
|
||||
auto cfg = liteConfig({alice.human()});
|
||||
cfg.LITE_DISCOVER = false;
|
||||
LiteWatch quiet{cfg, env.journal};
|
||||
env.trust(USD(100), alice);
|
||||
env.close();
|
||||
BEAST_EXPECT(quiet.discover(*env.closed()) == 0);
|
||||
BEAST_EXPECT(!quiet.isWatched(line));
|
||||
}
|
||||
|
||||
void
|
||||
testPartialSync()
|
||||
{
|
||||
testcase("partial sync");
|
||||
|
||||
using namespace jtx;
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
Env env{*this};
|
||||
env.fund(XRP(10000), gw, alice, bob);
|
||||
env.trust(USD(100), alice);
|
||||
env(pay(gw, alice, USD(50)));
|
||||
env.close();
|
||||
|
||||
auto const ledger =
|
||||
std::dynamic_pointer_cast<Ledger const>(env.closed());
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
auto const& full = ledger->stateMap();
|
||||
|
||||
LiteWatch watch{liteConfig({alice.human()}), env.journal};
|
||||
watch.discover(*ledger);
|
||||
auto const line = keylet::line(alice, gw, USD.currency).key;
|
||||
BEAST_EXPECT(watch.isWatched(line));
|
||||
|
||||
// A whole map needs nothing.
|
||||
BEAST_EXPECT(watch.missingNodes(full, 100).empty());
|
||||
|
||||
// A map holding just the root, in a family of its own so that
|
||||
// nothing resolves from a shared cache or store: what a lite node
|
||||
// holds after the root reply. Feed it what a peer would answer, one
|
||||
// round per level, until every watched key reads.
|
||||
tests::TestNodeFamily family{env.journal};
|
||||
SHAMap partial{SHAMapType::STATE, family};
|
||||
partial.setSynching();
|
||||
{
|
||||
Serializer s;
|
||||
full.serializeRoot(s);
|
||||
BEAST_EXPECT(partial.addRootNode(full.getHash(), s.slice(), nullptr)
|
||||
.isGood());
|
||||
}
|
||||
|
||||
int rounds = 0;
|
||||
std::size_t held = 1; // the root
|
||||
for (; rounds < 32; ++rounds)
|
||||
{
|
||||
auto const missing = watch.missingNodes(partial, 100);
|
||||
if (missing.empty())
|
||||
break;
|
||||
|
||||
held += missing.size();
|
||||
for (auto const& [id, hash] : missing)
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, Blob>> data;
|
||||
full.getNodeFat(id, data, false, 0);
|
||||
if (!BEAST_EXPECT(!data.empty()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
partial.addKnownNode(id, makeSlice(data[0].second), nullptr)
|
||||
.isGood());
|
||||
}
|
||||
}
|
||||
|
||||
// Converged, in no more rounds than the map is deep.
|
||||
BEAST_EXPECT(rounds > 0 && rounds < 32);
|
||||
BEAST_EXPECT(watch.missingNodes(partial, 100).empty());
|
||||
BEAST_EXPECT(partial.getHash() == full.getHash());
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
BEAST_EXPECT(partial.peekItemPartial(line, gap) && !gap);
|
||||
BEAST_EXPECT(
|
||||
partial.peekItemPartial(keylet::account(alice).key, gap) && !gap);
|
||||
BEAST_EXPECT(partial.peekItemPartial(keylet::fees().key, gap) && !gap);
|
||||
|
||||
// bob was never asked for: his path stops at a node not held.
|
||||
BEAST_EXPECT(
|
||||
!partial.peekItemPartial(keylet::account(bob).key, gap) && gap);
|
||||
|
||||
// And the partial map is a fraction of the whole one. (visitNodes()
|
||||
// cannot be asked of the partial map: it dereferences every child.)
|
||||
std::size_t whole = 0;
|
||||
full.visitNodes([&](SHAMapTreeNode&) {
|
||||
++whole;
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(held < whole);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testConfigLines();
|
||||
testWatchSet();
|
||||
testDiscover();
|
||||
testPartialSync();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LiteWatch, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -133,6 +133,32 @@ private:
|
||||
void
|
||||
trigger(std::shared_ptr<Peer> const&, TriggerReason);
|
||||
|
||||
/** The [lite_mode] acquire: header, whole transaction map, then only
|
||||
the state nodes on the paths to the watched keys, which the
|
||||
transactions just fetched may have added to. Called by trigger()
|
||||
once the header is held, with mtx_ locked and tmGL prepared. */
|
||||
void
|
||||
liteTrigger(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL);
|
||||
|
||||
/** The state nodes still needed to read every watched key. */
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
liteMissingStateNodes(std::size_t max) const;
|
||||
|
||||
/** Ask peers for the nodes the early manifest read needs, if any.
|
||||
@return true if a request was sent, and so the caller should return;
|
||||
false if the manifests are read, being read, not wanted, or
|
||||
the nodes are all recently asked for */
|
||||
bool
|
||||
earlyManifestSync(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL);
|
||||
|
||||
std::vector<neededHash_t>
|
||||
getNeededHashes();
|
||||
|
||||
@@ -209,6 +235,12 @@ private:
|
||||
*/
|
||||
std::atomic<bool> mManifestsSynced{false};
|
||||
|
||||
/** [lite_mode]: the transactions of this ledger have been read for
|
||||
objects the watch set should grow by. Done once, before the state
|
||||
paths are requested, so that an object created in this ledger is
|
||||
fetched from this ledger. */
|
||||
bool mLiteDiscovered{false};
|
||||
|
||||
bool mHaveHeader;
|
||||
bool mHaveState;
|
||||
bool mHaveTransactions;
|
||||
|
||||
@@ -447,6 +447,11 @@ private:
|
||||
// ledgers are retained in memory (see mRetainedLedgers).
|
||||
bool const nullBackend_;
|
||||
|
||||
// [lite_mode]: ledgers hold only the state paths to the watched keys.
|
||||
// They are never marked full, never backfilled, and the validated
|
||||
// ledger doubles as the closed ledger since no consensus round runs.
|
||||
bool const liteMode_;
|
||||
|
||||
std::uint32_t const ledger_fetch_size_;
|
||||
|
||||
TaggedCache<uint256, Blob> fetch_packs_;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_LEDGER_LITEWATCH_H_INCLUDED
|
||||
#define RIPPLE_APP_LEDGER_LITEWATCH_H_INCLUDED
|
||||
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Config;
|
||||
class ReadView;
|
||||
|
||||
/** The set of ledger objects a lite node tracks.
|
||||
|
||||
A node in [lite_mode] does not hold a ledger's state map. For each
|
||||
validated ledger it fetches the header, the whole transaction map (small,
|
||||
and needed to answer `tx` and account subscriptions), and only the state
|
||||
map nodes on the paths from the root to the keys in this set. Everything
|
||||
else about the ledger stays on the network.
|
||||
|
||||
The set has three tiers:
|
||||
|
||||
- System keys every ledger needs to be usable at all: the fee settings,
|
||||
the amendments object, the skip list and the negative UNL. Always
|
||||
present, never removed.
|
||||
- Configured keys from [lite_watch] and the lite_watch RPC: account
|
||||
roots, raw keylets and trust lines. Removed only by lite_unwatch.
|
||||
- Discovered keys: objects that transactions touching a watched account
|
||||
created or modified (a trust line opened to it, an escrow it is the
|
||||
destination of, hook state its hooks wrote). Added by discover() as
|
||||
each ledger's transactions arrive, removed again when the object is
|
||||
deleted.
|
||||
|
||||
All methods are thread safe. The walk over the state map in
|
||||
missingNodes() reads a map that InboundLedger is still adding nodes to,
|
||||
which is what SHAMap::getMissingNodes() already does.
|
||||
*/
|
||||
class LiteWatch
|
||||
{
|
||||
public:
|
||||
enum class Tier { system, configured, discovered };
|
||||
|
||||
struct Entry
|
||||
{
|
||||
uint256 key;
|
||||
Tier tier;
|
||||
/** Why it is watched, for lite_status: "account rXXX", "system
|
||||
fees", "created by <txid>", ... */
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
/** Most keys the set will hold, system keys included. Discovery stops
|
||||
adding at this point rather than let one busy issuer account drag a
|
||||
Raspberry Pi into fetching its whole owner tree. */
|
||||
static constexpr std::size_t maxKeys = 16384;
|
||||
|
||||
LiteWatch(Config const& config, beast::Journal journal);
|
||||
|
||||
/** True when the server runs in [lite_mode]. */
|
||||
bool
|
||||
enabled() const
|
||||
{
|
||||
return enabled_;
|
||||
}
|
||||
|
||||
/** Watch an account: its AccountRoot and Hook object, plus discovery of
|
||||
the objects its transactions touch. Returns false if already watched
|
||||
or full. */
|
||||
bool
|
||||
watchAccount(AccountID const& id, Tier tier = Tier::configured);
|
||||
|
||||
/** Watch one ledger object by key. */
|
||||
bool
|
||||
watchKey(
|
||||
uint256 const& key,
|
||||
Tier tier = Tier::configured,
|
||||
std::string reason = {});
|
||||
|
||||
/** Stop watching a key. System keys cannot be removed. Returns true if
|
||||
something was removed. */
|
||||
bool
|
||||
unwatchKey(uint256 const& key);
|
||||
|
||||
/** Stop watching an account and its root. Objects discovered through it
|
||||
are dropped as well; configured keys are kept. */
|
||||
bool
|
||||
unwatchAccount(AccountID const& id);
|
||||
|
||||
bool
|
||||
isWatched(uint256 const& key) const;
|
||||
|
||||
bool
|
||||
isWatchedAccount(AccountID const& id) const;
|
||||
|
||||
/** Every key currently watched. */
|
||||
std::vector<uint256>
|
||||
keys() const;
|
||||
|
||||
/** Every entry, for lite_status. */
|
||||
std::vector<Entry>
|
||||
entries() const;
|
||||
|
||||
std::size_t
|
||||
size() const;
|
||||
|
||||
/** The nodes a partly held state map still needs to read every watched
|
||||
key. One entry per node, however many keys it blocks; at most `max`.
|
||||
Empty once every watched key can be read (or shown absent).
|
||||
|
||||
@param stateMap State map with at least its root node
|
||||
@param max Most nodes to report
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingNodes(SHAMap const& stateMap, std::size_t max) const;
|
||||
|
||||
/** Learn from one validated transaction and its metadata which objects
|
||||
are now of interest.
|
||||
|
||||
- An affected node any of whose account fields name a watched account
|
||||
(Account, Owner, Destination, Issuer, the two limits of a trust
|
||||
line, ...) is watched from now on.
|
||||
- If the transaction was about a watched account (sent by it, ran a
|
||||
hook of it, or wrote an object naming it), every node it created
|
||||
is watched too: a new trust line, escrow, check, offer, URIToken
|
||||
or hook state belonging to the account is almost always among
|
||||
them, and a transaction creates few objects.
|
||||
- A deleted node that was only discovered is forgotten.
|
||||
|
||||
@return number of keys added
|
||||
*/
|
||||
std::size_t
|
||||
discover(STTx const& tx, STObject const& meta, std::uint32_t ledgerSeq);
|
||||
|
||||
/** discover() over every transaction of a ledger whose transaction map
|
||||
is complete. Reading the state map is not required. */
|
||||
std::size_t
|
||||
discover(ReadView const& ledger);
|
||||
|
||||
Json::Value
|
||||
getJson() const;
|
||||
|
||||
private:
|
||||
bool
|
||||
addLocked(uint256 const& key, Tier tier, std::string reason);
|
||||
|
||||
bool const enabled_;
|
||||
bool const discoverEnabled_;
|
||||
beast::Journal j_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
hash_map<uint256, Entry> keys_;
|
||||
hash_set<AccountID> accounts_;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/TransactionStateSF.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
@@ -244,10 +245,33 @@ InboundLedger::neededTxHashes(int max, SHAMapSyncFilter* filter) const
|
||||
std::vector<uint256>
|
||||
InboundLedger::neededStateHashes(int max, SHAMapSyncFilter* filter) const
|
||||
{
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
// Only the paths to the watched keys are wanted, so a walk of the
|
||||
// whole map (which would also mark subtrees full below) is never
|
||||
// the right question to ask of it.
|
||||
std::vector<uint256> ret;
|
||||
if (mLedger->info().accountHash.isNonZero())
|
||||
{
|
||||
if (mLedger->stateMap().getHash().isZero())
|
||||
ret.push_back(mLedger->info().accountHash);
|
||||
else
|
||||
for (auto const& n : liteMissingStateNodes(max))
|
||||
ret.push_back(n.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return neededHashes(
|
||||
mLedger->info().accountHash, mLedger->stateMap(), max, filter);
|
||||
}
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
InboundLedger::liteMissingStateNodes(std::size_t max) const
|
||||
{
|
||||
return app_.getLiteWatch().missingNodes(mLedger->stateMap(), max);
|
||||
}
|
||||
|
||||
// See how much of the ledger data is stored locally
|
||||
// Data found in a fetch pack will be stored
|
||||
void
|
||||
@@ -638,6 +662,12 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
else
|
||||
tmGL.set_querydepth(1);
|
||||
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
liteTrigger(sl, peer, reason, tmGL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the state data first because it's the most likely to be useful
|
||||
// if we wind up abandoning this fetch.
|
||||
if (mHaveHeader && !mHaveState && !failed_)
|
||||
@@ -675,50 +705,8 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
// key as well as through the directory. They are what a node with
|
||||
// no manifests at all needs, and reading them by key does not
|
||||
// depend on how long the directory has grown.
|
||||
if (!mManifestsSynced && app_.config().EARLY_MANIFEST_SYNC &&
|
||||
wantManifests())
|
||||
{
|
||||
sl.unlock();
|
||||
auto listed = listedMasterKeys(app_);
|
||||
std::vector<uint256> targets;
|
||||
targets.reserve(listed.size());
|
||||
for (auto const& pk : listed)
|
||||
targets.push_back(keylet::manifest(pk).key);
|
||||
auto nodes = missingManifestNodes(
|
||||
mLedger->stateMap(),
|
||||
missingNodesFind,
|
||||
targets,
|
||||
manifestSyncPages);
|
||||
sl.lock();
|
||||
|
||||
if (!failed_ && !complete_ && !mHaveState && !mManifestsSynced)
|
||||
{
|
||||
if (nodes.empty())
|
||||
{
|
||||
syncManifests(std::move(listed), manifestSyncPages);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
// All recently asked for: fall through to the rest of
|
||||
// the map rather than wait on them.
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Sending manifest node request ("
|
||||
<< nodes.size() << ") to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (earlyManifestSync(sl, peer, reason, tmGL))
|
||||
return;
|
||||
|
||||
AccountStateSF filter(
|
||||
mLedger->stateMap().family().db(), app_.getLedgerMaster());
|
||||
@@ -848,6 +836,216 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
InboundLedger::earlyManifestSync(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL)
|
||||
{
|
||||
if (mManifestsSynced || !app_.config().EARLY_MANIFEST_SYNC ||
|
||||
!wantManifests())
|
||||
return false;
|
||||
|
||||
sl.unlock();
|
||||
auto listed = listedMasterKeys(app_);
|
||||
std::vector<uint256> targets;
|
||||
targets.reserve(listed.size());
|
||||
for (auto const& pk : listed)
|
||||
targets.push_back(keylet::manifest(pk).key);
|
||||
auto nodes = missingManifestNodes(
|
||||
mLedger->stateMap(), missingNodesFind, targets, manifestSyncPages);
|
||||
sl.lock();
|
||||
|
||||
// Make sure nothing happened while we released the lock
|
||||
if (failed_ || complete_ || mHaveState || mManifestsSynced)
|
||||
return false;
|
||||
|
||||
if (nodes.empty())
|
||||
{
|
||||
syncManifests(std::move(listed), manifestSyncPages);
|
||||
return false;
|
||||
}
|
||||
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
// All recently asked for: fall through to the rest of the map rather
|
||||
// than wait on them.
|
||||
if (nodes.empty())
|
||||
return false;
|
||||
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace()) << "Sending manifest node request (" << nodes.size()
|
||||
<< ") to " << (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::liteTrigger(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
failed_ || (mHaveHeader && mLedger),
|
||||
"ripple::InboundLedger::liteTrigger : header held");
|
||||
|
||||
// Transactions first, and all of them. They are what the watchers are
|
||||
// waiting to see, and they name the objects this ledger created for the
|
||||
// watched accounts, which must be known before the state paths are
|
||||
// asked for if they are to be read from this ledger.
|
||||
if (!mHaveTransactions && !failed_)
|
||||
{
|
||||
if (!mLedger->txMap().isValid())
|
||||
{
|
||||
failed_ = true;
|
||||
}
|
||||
else if (mLedger->txMap().getHash().isZero())
|
||||
{
|
||||
tmGL.set_itype(protocol::liTX_NODE);
|
||||
*(tmGL.add_nodeids()) = SHAMapNodeID().getRawString();
|
||||
JLOG(journal_.trace()) << "Lite: sending TX root request to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
TransactionStateSF filter(
|
||||
mLedger->txMap().family().db(), app_.getLedgerMaster());
|
||||
|
||||
auto nodes =
|
||||
mLedger->txMap().getMissingNodes(missingNodesFind, &filter);
|
||||
|
||||
if (nodes.empty())
|
||||
{
|
||||
if (!mLedger->txMap().isValid())
|
||||
failed_ = true;
|
||||
else
|
||||
mHaveTransactions = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liTX_NODE);
|
||||
for (auto const& n : nodes)
|
||||
*(tmGL.add_nodeids()) = n.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Lite: sending TX node request (" << nodes.size()
|
||||
<< ") to " << (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(journal_.trace()) << "Lite: all TX nodes filtered";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grow the watch set by what this ledger's transactions did to the
|
||||
// watched accounts. Reading the complete transaction map is what
|
||||
// getMissingNodes() just did, so the lock can be released for it.
|
||||
if (mHaveTransactions && !mLiteDiscovered && !failed_)
|
||||
{
|
||||
mLiteDiscovered = true;
|
||||
sl.unlock();
|
||||
auto const added = app_.getLiteWatch().discover(*mLedger);
|
||||
sl.lock();
|
||||
|
||||
if (added != 0)
|
||||
{
|
||||
JLOG(journal_.debug()) << "Lite: ledger " << mLedger->info().seq
|
||||
<< " adds " << added << " watched keys";
|
||||
}
|
||||
}
|
||||
|
||||
if (mHaveTransactions && !mHaveState && !failed_)
|
||||
{
|
||||
if (!mLedger->stateMap().isValid())
|
||||
{
|
||||
failed_ = true;
|
||||
}
|
||||
else if (mLedger->stateMap().getHash().isZero())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
*tmGL.add_nodeids() = SHAMapNodeID().getRawString();
|
||||
JLOG(journal_.trace()) << "Lite: sending AS root request to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Before the first validated ledger the manifests are what
|
||||
// decide which validations to believe; they are a few paths
|
||||
// more, read the same way.
|
||||
if (earlyManifestSync(sl, peer, reason, tmGL))
|
||||
return;
|
||||
|
||||
// The paths to the watched keys, and nothing else. Unchanged
|
||||
// paths resolve from the tree node cache, which is keyed by
|
||||
// hash, so in steady state this costs the nodes near the root
|
||||
// that every ledger rewrites.
|
||||
sl.unlock();
|
||||
auto nodes = liteMissingStateNodes(missingNodesFind);
|
||||
sl.lock();
|
||||
|
||||
// Make sure nothing happened while we released the lock
|
||||
if (!failed_ && !complete_ && !mHaveState)
|
||||
{
|
||||
if (nodes.empty())
|
||||
{
|
||||
mHaveState = true;
|
||||
complete_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Lite: sending AS path request (" << nodes.size()
|
||||
<< ") to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(journal_.trace()) << "Lite: all AS nodes filtered";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (mHaveTransactions && mHaveState && !failed_)
|
||||
{
|
||||
// A ledger with no state root, or one tryDB() found whole.
|
||||
complete_ = true;
|
||||
}
|
||||
|
||||
if (complete_ || failed_)
|
||||
{
|
||||
JLOG(journal_.debug()) << "Lite done:" << (complete_ ? " complete" : "")
|
||||
<< (failed_ ? " failed " : " ")
|
||||
<< (mLedger ? mLedger->info().seq : mSeq);
|
||||
sl.unlock();
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
InboundLedger::wantManifests()
|
||||
{
|
||||
|
||||
@@ -114,6 +114,7 @@ LedgerMaster::LedgerMaster(
|
||||
app_.getSHAMapStore().clampFetchDepth(app_.config().FETCH_DEPTH))
|
||||
, ledger_history_(app_.config().LEDGER_HISTORY)
|
||||
, nullBackend_(app_.config().nullBackend())
|
||||
, liteMode_(app_.config().liteMode())
|
||||
, ledger_fetch_size_(app_.config().getValueFor(SizedItem::ledgerFetch))
|
||||
, fetch_packs_(
|
||||
"FetchPack",
|
||||
@@ -278,6 +279,28 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
|
||||
|
||||
mValidLedger.set(l);
|
||||
mValidLedgerSign = signTime.time_since_epoch().count();
|
||||
|
||||
// A lite node runs no consensus round, so nothing else ever sets a
|
||||
// closed ledger or moves the open ledger on. The validated ledger is the
|
||||
// most recent one it holds: it is the closed ledger, and the open ledger
|
||||
// is an empty view on top of it, so that `current` reads, sequence and
|
||||
// fee autofill see the watched accounts as they now are. Nothing is
|
||||
// applied to it: a transaction submitted to a lite node is relayed, not
|
||||
// applied (NetworkOPsImp::apply).
|
||||
if (liteMode_)
|
||||
{
|
||||
mClosedLedger.set(l);
|
||||
OrderedTxs retries{uint256{}};
|
||||
app_.openLedger().accept(
|
||||
app_,
|
||||
l->rules(),
|
||||
l,
|
||||
OrderedTxs({}),
|
||||
false,
|
||||
retries,
|
||||
tapNONE,
|
||||
"lite");
|
||||
}
|
||||
XRPL_ASSERT(
|
||||
mValidLedgerSeq || !app_.getMaxDisallowedLedger() ||
|
||||
l->info().seq + max_ledger_difference_ >
|
||||
@@ -920,7 +943,11 @@ LedgerMaster::setFullLedger(
|
||||
"ripple::LedgerMaster::setFullLedger : nonzero ledger state hash");
|
||||
|
||||
ledger->setValidated();
|
||||
ledger->setFull();
|
||||
|
||||
// A lite ledger holds only the watched paths: marking it full would make
|
||||
// the first read of anything else trigger a fresh acquire.
|
||||
if (!liteMode_)
|
||||
ledger->setFull();
|
||||
|
||||
if (isCurrent)
|
||||
mLedgerHistory.insert(ledger, true);
|
||||
@@ -1099,7 +1126,8 @@ LedgerMaster::checkAccept(std::shared_ptr<Ledger const> const& ledger)
|
||||
<< " validations";
|
||||
|
||||
ledger->setValidated();
|
||||
ledger->setFull();
|
||||
if (!liteMode_)
|
||||
ledger->setFull();
|
||||
setValidLedger(ledger);
|
||||
|
||||
JLOG(m_journal.info()) << "checkAccept (" << ledger->info().seq
|
||||
@@ -2272,7 +2300,10 @@ LedgerMaster::doAdvance(std::unique_lock<std::recursive_mutex>& sl)
|
||||
auto const pubLedgers = findNewLedgersToPublish(sl);
|
||||
if (pubLedgers.empty())
|
||||
{
|
||||
if (!standalone_ && !app_.getFeeTrack().isLoadedLocal() &&
|
||||
// A lite node keeps no history: it serves nobody, and every
|
||||
// ledger it fetched would cost the same paths over again.
|
||||
if (!standalone_ && !liteMode_ &&
|
||||
!app_.getFeeTrack().isLoadedLocal() &&
|
||||
(app_.getJobQueue().getJobCount(jtPUBOLDLEDGER) < 10) &&
|
||||
(mValidLedgerSeq == mPubLedgerSeq) &&
|
||||
(getValidatedLedgerAge() < MAX_LEDGER_AGE_ACQUIRE) &&
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
LiteWatch::LiteWatch(Config const& config, beast::Journal journal)
|
||||
: enabled_(config.LITE_MODE)
|
||||
, discoverEnabled_(config.LITE_DISCOVER)
|
||||
, j_(journal)
|
||||
{
|
||||
if (!enabled_)
|
||||
return;
|
||||
|
||||
// Read by Ledger::setup(), AmendmentTable::doValidatedLedger(),
|
||||
// hashOfSeq() and LedgerMaster::setValidLedger() on every ledger: without
|
||||
// them a ledger cannot even be accepted.
|
||||
addLocked(keylet::fees().key, Tier::system, "system fees");
|
||||
addLocked(keylet::amendments().key, Tier::system, "system amendments");
|
||||
addLocked(keylet::skip().key, Tier::system, "system skip list");
|
||||
addLocked(keylet::negativeUNL().key, Tier::system, "system negative UNL");
|
||||
|
||||
for (auto const& line : config.LITE_WATCH)
|
||||
{
|
||||
std::string err;
|
||||
auto const parsed = parseLiteWatchLine(line, err);
|
||||
if (!parsed)
|
||||
{
|
||||
// Config::loadFromString() has already rejected these.
|
||||
JLOG(j_.warn()) << "[" SECTION_LITE_WATCH "] " << err;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed->first)
|
||||
{
|
||||
accounts_.insert(*parsed->first);
|
||||
auto const reason = "account " + toBase58(*parsed->first);
|
||||
addLocked(parsed->second, Tier::configured, reason);
|
||||
addLocked(
|
||||
keylet::hook(*parsed->first).key, Tier::configured, reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
addLocked(parsed->second, Tier::configured, "keylet");
|
||||
}
|
||||
}
|
||||
|
||||
JLOG(j_.info()) << "Lite mode: watching " << keys_.size() << " keys ("
|
||||
<< accounts_.size() << " accounts)";
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::addLocked(uint256 const& key, Tier tier, std::string reason)
|
||||
{
|
||||
auto it = keys_.find(key);
|
||||
if (it != keys_.end())
|
||||
{
|
||||
// Promote: a discovered object the operator then asks for by name
|
||||
// must survive the deletion of whatever it was discovered through.
|
||||
if (static_cast<int>(tier) < static_cast<int>(it->second.tier))
|
||||
{
|
||||
it->second.tier = tier;
|
||||
it->second.reason = std::move(reason);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keys_.size() >= maxKeys)
|
||||
{
|
||||
JLOG(j_.warn()) << "Lite watch set is full (" << maxKeys
|
||||
<< " keys); not watching " << key;
|
||||
return false;
|
||||
}
|
||||
|
||||
keys_.emplace(key, Entry{key, tier, std::move(reason)});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::watchAccount(AccountID const& id, Tier tier)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
bool const added = accounts_.insert(id).second;
|
||||
auto const reason = "account " + toBase58(id);
|
||||
|
||||
// The root, and the Hook object that lists the hooks installed on it:
|
||||
// the base fee of anything the account sends depends on those, and a
|
||||
// submit autofilling its fee reads them.
|
||||
bool const root = addLocked(keylet::account(id).key, tier, reason);
|
||||
bool const hook = addLocked(keylet::hook(id).key, tier, reason);
|
||||
return root || hook || added;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::watchKey(uint256 const& key, Tier tier, std::string reason)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return addLocked(key, tier, reason.empty() ? "keylet" : std::move(reason));
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::unwatchKey(uint256 const& key)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = keys_.find(key);
|
||||
if (it == keys_.end() || it->second.tier == Tier::system)
|
||||
return false;
|
||||
keys_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::unwatchAccount(AccountID const& id)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
bool removed = accounts_.erase(id) != 0;
|
||||
|
||||
for (auto const& k : {keylet::account(id).key, keylet::hook(id).key})
|
||||
{
|
||||
auto it = keys_.find(k);
|
||||
if (it != keys_.end() && it->second.tier != Tier::system)
|
||||
{
|
||||
keys_.erase(it);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Objects only known through this account go with it. Everything an
|
||||
// operator asked for by key stays.
|
||||
for (auto i = keys_.begin(); i != keys_.end();)
|
||||
{
|
||||
if (i->second.tier == Tier::discovered)
|
||||
{
|
||||
i = keys_.erase(i);
|
||||
removed = true;
|
||||
}
|
||||
else
|
||||
++i;
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::isWatched(uint256 const& key) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return keys_.contains(key);
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::isWatchedAccount(AccountID const& id) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return accounts_.contains(id);
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
LiteWatch::keys() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::vector<uint256> ret;
|
||||
ret.reserve(keys_.size());
|
||||
for (auto const& [key, entry] : keys_)
|
||||
ret.push_back(key);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<LiteWatch::Entry>
|
||||
LiteWatch::entries() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::vector<Entry> ret;
|
||||
ret.reserve(keys_.size());
|
||||
for (auto const& [key, entry] : keys_)
|
||||
ret.push_back(entry);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LiteWatch::size() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return keys_.size();
|
||||
}
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
LiteWatch::missingNodes(SHAMap const& stateMap, std::size_t max) const
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>> missing;
|
||||
|
||||
// Snapshot the keys: the walk reads the map, not the set, and a
|
||||
// discovery running on another ledger must not wait on it.
|
||||
auto const wanted = keys();
|
||||
|
||||
// By hash: several keys blocked by the same node report it once. Near
|
||||
// the root that is every key.
|
||||
hash_set<uint256> reported;
|
||||
|
||||
for (auto const& key : wanted)
|
||||
{
|
||||
if (missing.size() >= max)
|
||||
break;
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
stateMap.peekItemPartial(key, gap);
|
||||
if (gap && reported.insert(gap->second).second)
|
||||
missing.push_back(*gap);
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LiteWatch::discover(
|
||||
STTx const& tx,
|
||||
STObject const& meta,
|
||||
std::uint32_t ledgerSeq)
|
||||
{
|
||||
if (!discoverEnabled_ || !meta.isFieldPresent(sfAffectedNodes))
|
||||
return 0;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto const watchedAccount = [&](AccountID const& id) {
|
||||
return accounts_.contains(id);
|
||||
};
|
||||
|
||||
// Is this transaction about a watched account? Sent by one, run a hook
|
||||
// of one, or wrote an object that names one. Merely modifying a key
|
||||
// already watched is not enough: a trust line set against a watched
|
||||
// issuer touches the issuer's owner directory, and that must not pull
|
||||
// in every stranger's line.
|
||||
bool touches = watchedAccount(tx.getAccountID(sfAccount));
|
||||
|
||||
if (!touches && meta.isFieldPresent(sfHookExecutions))
|
||||
{
|
||||
for (auto const& exec : meta.getFieldArray(sfHookExecutions))
|
||||
{
|
||||
if (exec.isFieldPresent(sfHookAccount) &&
|
||||
watchedAccount(exec.getAccountID(sfHookAccount)))
|
||||
{
|
||||
touches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto const& nodes = meta.getFieldArray(sfAffectedNodes);
|
||||
|
||||
// Pass 1: nodes that name a watched account, and whether anything
|
||||
// watched was touched.
|
||||
std::vector<uint256> wanted;
|
||||
std::vector<uint256> deleted;
|
||||
|
||||
for (auto const& node : nodes)
|
||||
{
|
||||
if (!node.isFieldPresent(sfLedgerIndex))
|
||||
continue;
|
||||
|
||||
auto const index = node.getFieldH256(sfLedgerIndex);
|
||||
bool const isDeleted = node.getFName() == sfDeletedNode;
|
||||
|
||||
if (keys_.contains(index))
|
||||
{
|
||||
if (isDeleted)
|
||||
deleted.push_back(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDeleted)
|
||||
continue;
|
||||
|
||||
int const fieldsIndex = node.getFieldIndex(
|
||||
node.getFName() == sfCreatedNode ? sfNewFields : sfFinalFields);
|
||||
if (fieldsIndex == -1)
|
||||
continue;
|
||||
|
||||
auto const inner =
|
||||
dynamic_cast<STObject const*>(&node.peekAtIndex(fieldsIndex));
|
||||
if (!inner)
|
||||
continue;
|
||||
|
||||
bool const isLine = node.isFieldPresent(sfLedgerEntryType) &&
|
||||
node.getFieldU16(sfLedgerEntryType) == ltRIPPLE_STATE;
|
||||
|
||||
bool names = false;
|
||||
for (auto const& field : *inner)
|
||||
{
|
||||
if (auto const sa = dynamic_cast<STAccount const*>(&field))
|
||||
{
|
||||
if (!sa->isDefault() && watchedAccount(sa->value()))
|
||||
{
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (
|
||||
isLine &&
|
||||
(field.getFName() == sfLowLimit ||
|
||||
field.getFName() == sfHighLimit))
|
||||
{
|
||||
// The two parties of a trust line are the issuers of its
|
||||
// limits. Not done for offers: an issuer's every order book
|
||||
// entry is not what "my account" means.
|
||||
if (auto const lim = dynamic_cast<STAmount const*>(&field);
|
||||
lim && watchedAccount(lim->getIssuer()))
|
||||
{
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (names)
|
||||
{
|
||||
touches = true;
|
||||
wanted.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: a transaction that touched something watched created these.
|
||||
if (touches)
|
||||
{
|
||||
for (auto const& node : nodes)
|
||||
{
|
||||
if (node.getFName() == sfCreatedNode &&
|
||||
node.isFieldPresent(sfLedgerIndex))
|
||||
wanted.push_back(node.getFieldH256(sfLedgerIndex));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t added = 0;
|
||||
std::string const reason = "tx " + to_string(tx.getTransactionID()) +
|
||||
" ledger " + std::to_string(ledgerSeq);
|
||||
for (auto const& key : wanted)
|
||||
if (addLocked(key, Tier::discovered, reason))
|
||||
++added;
|
||||
|
||||
for (auto const& key : deleted)
|
||||
{
|
||||
auto it = keys_.find(key);
|
||||
if (it != keys_.end() && it->second.tier == Tier::discovered)
|
||||
keys_.erase(it);
|
||||
}
|
||||
|
||||
if (added != 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "Lite mode: " << reason << " adds " << added
|
||||
<< " keys (" << keys_.size() << " watched)";
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LiteWatch::discover(ReadView const& ledger)
|
||||
{
|
||||
if (!discoverEnabled_)
|
||||
return 0;
|
||||
|
||||
std::size_t added = 0;
|
||||
for (auto const& [tx, meta] : ledger.txs)
|
||||
{
|
||||
if (tx && meta)
|
||||
added += discover(*tx, *meta, ledger.seq());
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
LiteWatch::getJson() const
|
||||
{
|
||||
Json::Value ret{Json::objectValue};
|
||||
ret[jss::enabled] = enabled_;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
ret["discover"] = discoverEnabled_;
|
||||
ret["key_count"] = static_cast<Json::UInt>(keys_.size());
|
||||
ret["max_keys"] = static_cast<Json::UInt>(maxKeys);
|
||||
|
||||
Json::Value accounts{Json::arrayValue};
|
||||
for (auto const& id : accounts_)
|
||||
accounts.append(toBase58(id));
|
||||
ret[jss::accounts] = std::move(accounts);
|
||||
|
||||
std::size_t system = 0, configured = 0, discovered = 0;
|
||||
Json::Value keys{Json::arrayValue};
|
||||
for (auto const& [key, entry] : keys_)
|
||||
{
|
||||
switch (entry.tier)
|
||||
{
|
||||
case Tier::system:
|
||||
++system;
|
||||
break;
|
||||
case Tier::configured:
|
||||
++configured;
|
||||
break;
|
||||
case Tier::discovered:
|
||||
++discovered;
|
||||
break;
|
||||
}
|
||||
Json::Value k{Json::objectValue};
|
||||
k[jss::key] = to_string(key);
|
||||
k["tier"] = entry.tier == Tier::system ? "system"
|
||||
: entry.tier == Tier::configured ? "configured"
|
||||
: "discovered";
|
||||
k["reason"] = entry.reason;
|
||||
keys.append(std::move(k));
|
||||
}
|
||||
ret["system"] = static_cast<Json::UInt>(system);
|
||||
ret["configured"] = static_cast<Json::UInt>(configured);
|
||||
ret["discovered"] = static_cast<Json::UInt>(discovered);
|
||||
ret["keys"] = std::move(keys);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LedgerReplayer.h>
|
||||
#include <xrpld/app/ledger/LedgerToJson.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/PendingSaves.h>
|
||||
@@ -217,6 +218,7 @@ public:
|
||||
RCLValidations mValidations;
|
||||
std::unique_ptr<LoadManager> m_loadManager;
|
||||
std::unique_ptr<TxQ> txQ_;
|
||||
std::unique_ptr<LiteWatch> liteWatch_;
|
||||
ClosureCounter<void, boost::system::error_code const&> waitHandlerCounter_;
|
||||
boost::asio::steady_timer sweepTimer_;
|
||||
boost::asio::steady_timer entropyTimer_;
|
||||
@@ -457,6 +459,10 @@ public:
|
||||
, txQ_(
|
||||
std::make_unique<TxQ>(setup_TxQ(*config_), logs_->journal("TxQ")))
|
||||
|
||||
, liteWatch_(std::make_unique<LiteWatch>(
|
||||
*config_,
|
||||
logs_->journal("LiteWatch")))
|
||||
|
||||
, sweepTimer_(get_io_context())
|
||||
|
||||
, entropyTimer_(get_io_context())
|
||||
@@ -811,6 +817,15 @@ public:
|
||||
return *txQ_;
|
||||
}
|
||||
|
||||
LiteWatch&
|
||||
getLiteWatch() override
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
liteWatch_,
|
||||
"ripple::ApplicationImp::getLiteWatch : non-null lite watch");
|
||||
return *liteWatch_;
|
||||
}
|
||||
|
||||
RelationalDatabase&
|
||||
getRelationalDatabase() override
|
||||
{
|
||||
|
||||
@@ -79,6 +79,7 @@ class Ledger;
|
||||
class LedgerMaster;
|
||||
class LedgerCleaner;
|
||||
class LedgerReplayer;
|
||||
class LiteWatch;
|
||||
class LoadManager;
|
||||
class ManifestCache;
|
||||
class ValidatorKeys;
|
||||
@@ -190,6 +191,8 @@ public:
|
||||
overlay() = 0;
|
||||
virtual TxQ&
|
||||
getTxQ() = 0;
|
||||
virtual LiteWatch&
|
||||
getLiteWatch() = 0;
|
||||
virtual ValidatorList&
|
||||
validators() = 0;
|
||||
virtual ValidatorSite&
|
||||
|
||||
@@ -1027,6 +1027,30 @@ NetworkOPsImp::processHeartbeatTimer()
|
||||
<< ", changing to " << strOperatingMode(newMode, true);
|
||||
}
|
||||
CLOG(clog.ss()) << ". ";
|
||||
|
||||
// A lite node holds no state to close a ledger from, so no
|
||||
// consensus round ever runs. It tracks the network by the validated
|
||||
// ledgers it fetches: in step while the last one is fresh.
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
auto const age = m_ledgerMaster.getValidatedLedgerAge();
|
||||
if (age < 1min)
|
||||
{
|
||||
if (mMode < OperatingMode::TRACKING)
|
||||
setMode(OperatingMode::TRACKING);
|
||||
}
|
||||
else if (mMode == OperatingMode::TRACKING)
|
||||
{
|
||||
setMode(OperatingMode::CONNECTED);
|
||||
}
|
||||
CLOG(clog.ss())
|
||||
<< "lite mode, validated ledger age " << age.count() << "s. ";
|
||||
lock.unlock();
|
||||
bootstrapManifests();
|
||||
setHeartbeatTimer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mConsensus.timerEntry(app_.timeKeeper().closeTime(), clog.ss());
|
||||
@@ -1566,9 +1590,28 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
|
||||
batchLock.unlock();
|
||||
|
||||
// A lite node cannot apply a transaction: doing so reads the sender's
|
||||
// root, the destination's, its hooks, directories and whatever else the
|
||||
// transactor touches, nearly all of which it does not hold. It checks
|
||||
// what can be checked without state (preflight, done by
|
||||
// checkValidity() on the way in) and relays the rest to the network,
|
||||
// reporting terLITE_RELAYED. The outcome is read from the validated
|
||||
// ledger it turns up in, through `tx` or an account subscription.
|
||||
bool const lite = app_.config().liteMode();
|
||||
|
||||
{
|
||||
std::unique_lock masterLock{app_.getMasterMutex(), std::defer_lock};
|
||||
bool changed = false;
|
||||
if (lite)
|
||||
{
|
||||
masterLock.lock();
|
||||
for (TransactionStatus& e : transactions)
|
||||
{
|
||||
e.result = terLITE_RELAYED;
|
||||
e.applied = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::unique_lock ledgerLock{
|
||||
m_ledgerMaster.peekMutex(), std::defer_lock};
|
||||
@@ -1656,6 +1699,16 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
JLOG(m_journal.info()) << "Transaction is obsolete";
|
||||
e.transaction->setStatus(OBSOLETE);
|
||||
}
|
||||
else if (e.result == terLITE_RELAYED)
|
||||
{
|
||||
// Neither held nor kept: a lite node has nothing to retry
|
||||
// it against, and LocalTxs::sweep() would read the sender
|
||||
// on every validated ledger.
|
||||
JLOG(m_journal.debug())
|
||||
<< "Transaction relayed by lite node, not applied";
|
||||
e.transaction->setStatus(HELD);
|
||||
addLocal = false;
|
||||
}
|
||||
else if (e.result == terQUEUED)
|
||||
{
|
||||
JLOG(m_journal.debug())
|
||||
@@ -1703,7 +1756,8 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
if ((e.applied ||
|
||||
((mMode != OperatingMode::FULL) &&
|
||||
(e.failType != FailHard::yes) && e.local) ||
|
||||
(e.result == terQUEUED)) &&
|
||||
(e.result == terQUEUED) ||
|
||||
(e.result == terLITE_RELAYED && e.local)) &&
|
||||
!enforceFailHard)
|
||||
{
|
||||
auto const toSkip =
|
||||
@@ -1735,7 +1789,9 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
}
|
||||
}
|
||||
|
||||
if (validatedLedgerIndex)
|
||||
// Not for a lite node: this reads the sender's root, which it
|
||||
// holds only if the sender is watched.
|
||||
if (validatedLedgerIndex && !lite)
|
||||
{
|
||||
auto [fee, accountSeq, availableSeq] =
|
||||
app_.getTxQ().getTxRequiredFeeAndSeq(
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/FeeUnits.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SystemParameters.h> // VFALCO Breaks levelization
|
||||
@@ -195,6 +196,15 @@ public:
|
||||
// ahead of the rest of its state. See [early_manifest_sync].
|
||||
bool EARLY_MANIFEST_SYNC = true;
|
||||
|
||||
// Lite mode: follow validated ledgers without holding their state maps,
|
||||
// fetching only the paths to the objects in [lite_watch] (and the ones
|
||||
// their transactions lead to). Implies [node_db] type=rwdb, no
|
||||
// pathfinding, no consensus participation and no history fetch. See
|
||||
// [lite_mode], [lite_watch] and [lite_discover].
|
||||
bool LITE_MODE = false;
|
||||
bool LITE_DISCOVER = true;
|
||||
std::vector<std::string> LITE_WATCH;
|
||||
|
||||
// True to ask peers not to relay current IP.
|
||||
bool PEER_PRIVATE = false;
|
||||
|
||||
@@ -373,6 +383,13 @@ public:
|
||||
return RUN_STANDALONE;
|
||||
}
|
||||
|
||||
/** True when the server runs in [lite_mode]. */
|
||||
bool
|
||||
liteMode() const
|
||||
{
|
||||
return LITE_MODE;
|
||||
}
|
||||
|
||||
bool
|
||||
mem_backend() const
|
||||
{
|
||||
@@ -445,6 +462,14 @@ public:
|
||||
FeeSetup
|
||||
setup_FeeVote(Section const& section);
|
||||
|
||||
/** Parse one [lite_watch] line: a base58 account or a 64 hex digit key.
|
||||
|
||||
@return the account (if the line was one) and the key to watch, or
|
||||
nothing with `error` set
|
||||
*/
|
||||
std::optional<std::pair<std::optional<AccountID>, uint256>>
|
||||
parseLiteWatchLine(std::string const& line, std::string& error);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -61,6 +61,9 @@ struct ConfigSection
|
||||
#define SECTION_IPS_FIXED "ips_fixed"
|
||||
#define SECTION_LEDGER_HISTORY "ledger_history"
|
||||
#define SECTION_LEDGER_REPLAY "ledger_replay"
|
||||
#define SECTION_LITE_DISCOVER "lite_discover"
|
||||
#define SECTION_LITE_MODE "lite_mode"
|
||||
#define SECTION_LITE_WATCH "lite_watch"
|
||||
#define SECTION_MAX_TRANSACTIONS "max_transactions"
|
||||
#define SECTION_NETWORK_ID "network_id"
|
||||
#define SECTION_NETWORK_QUORUM "network_quorum"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/format.hpp>
|
||||
@@ -256,6 +257,29 @@ Config::Config()
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::optional<AccountID>, uint256>>
|
||||
parseLiteWatchLine(std::string const& line, std::string& error)
|
||||
{
|
||||
std::string s = line;
|
||||
boost::algorithm::trim(s);
|
||||
|
||||
if (s.empty())
|
||||
{
|
||||
error = "empty line";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (auto const id = parseBase58<AccountID>(s))
|
||||
return std::make_pair(id, keylet::account(*id).key);
|
||||
|
||||
uint256 key;
|
||||
if (s.size() == 64 && key.parseHex(s))
|
||||
return std::make_pair(std::nullopt, key);
|
||||
|
||||
error = "not a base58 account or a 64 hex digit key: " + s;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
Config::setupControl(bool bQuiet, bool bSilent, bool bStandalone)
|
||||
{
|
||||
@@ -694,6 +718,50 @@ Config::loadFromString(std::string const& fileContents)
|
||||
if (getSingleSection(secConfig, SECTION_PATH_SEARCH_MAX, strTemp, j_))
|
||||
PATH_SEARCH_MAX = beast::lexicalCastThrow<int>(strTemp);
|
||||
|
||||
if (getSingleSection(secConfig, SECTION_LITE_MODE, strTemp, j_))
|
||||
LITE_MODE = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
if (LITE_MODE)
|
||||
{
|
||||
if (RUN_STANDALONE)
|
||||
Throw<std::runtime_error>(
|
||||
"[" SECTION_LITE_MODE
|
||||
"] cannot be combined with standalone "
|
||||
"mode: a lite node follows validations and builds nothing");
|
||||
|
||||
if (exists(SECTION_VALIDATION_SEED) || exists(SECTION_VALIDATOR_TOKEN))
|
||||
Throw<std::runtime_error>(
|
||||
"[" SECTION_LITE_MODE
|
||||
"] cannot be combined with [" SECTION_VALIDATION_SEED
|
||||
"] or [" SECTION_VALIDATOR_TOKEN
|
||||
"]: a lite node does not hold the state it would validate");
|
||||
|
||||
if (getSingleSection(secConfig, SECTION_LITE_DISCOVER, strTemp, j_))
|
||||
LITE_DISCOVER = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
LITE_WATCH = section(SECTION_LITE_WATCH).lines();
|
||||
for (auto const& line : LITE_WATCH)
|
||||
{
|
||||
std::string err;
|
||||
if (!parseLiteWatchLine(line, err))
|
||||
Throw<std::runtime_error>("[" SECTION_LITE_WATCH "]: " + err);
|
||||
}
|
||||
|
||||
// A lite node never has the order books or the paths to search.
|
||||
PATH_SEARCH_MAX = 0;
|
||||
|
||||
// Partial state maps are held only by the retained Ledger objects.
|
||||
// The rwdb null node store is the one that keeps nothing on disk
|
||||
// and pins a ledger_history window in memory, so default to it, and
|
||||
// refuse a persistent store that would be fed partial trees.
|
||||
Section& nodeDb{section(ConfigSection::nodeDatabase())};
|
||||
if (nodeDb.empty())
|
||||
nodeDb.set("type", "rwdb");
|
||||
else if (!nullBackend())
|
||||
Throw<std::runtime_error>("[" SECTION_LITE_MODE
|
||||
"] requires [node_db] type=rwdb");
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, SECTION_DEBUG_LOGFILE, strTemp, j_))
|
||||
DEBUG_LOGFILE = strTemp;
|
||||
|
||||
|
||||
@@ -1414,6 +1414,16 @@ PeerImp::handleTransaction(
|
||||
void
|
||||
PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
|
||||
{
|
||||
// A lite node holds partial ledgers and builds no transaction sets. It
|
||||
// never advertises a ledger range, so a request here is a peer guessing
|
||||
// from its closed-ledger hash; answer with silence rather than with a
|
||||
// tree that would throw on the first node it does not hold.
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
JLOG(p_journal_.trace()) << "TMGetLedger: ignored in lite mode";
|
||||
return;
|
||||
}
|
||||
|
||||
auto badData = [&](std::string const& msg) {
|
||||
fee_.update(Resource::feeInvalidData, "get_ledger " + msg);
|
||||
JLOG(p_journal_.warn()) << "TMGetLedger: " << msg;
|
||||
@@ -2462,6 +2472,17 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
|
||||
return;
|
||||
}
|
||||
|
||||
// A lite node has no fetch packs to build and no node store to
|
||||
// look objects up in. Transaction requests (reduce-relay) are still
|
||||
// served from the transaction master.
|
||||
if (app_.config().liteMode() &&
|
||||
packet.type() != protocol::TMGetObjectByHash::otTRANSACTIONS)
|
||||
{
|
||||
JLOG(p_journal_.trace())
|
||||
<< "TMGetObjectByHash: ignored in lite mode";
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.type() == protocol::TMGetObjectByHash::otFETCH_PACK)
|
||||
{
|
||||
doFetchPack(m);
|
||||
|
||||
@@ -133,6 +133,9 @@ Handler const handlerArray[]{
|
||||
{"ledger_entry", byRef(&doLedgerEntry), Role::USER, NO_CONDITION},
|
||||
{"ledger_header", byRef(&doLedgerHeader), Role::USER, NO_CONDITION, 1, 1},
|
||||
{"ledger_request", byRef(&doLedgerRequest), Role::ADMIN, NO_CONDITION},
|
||||
{"lite_status", byRef(&doLiteStatus), Role::USER, NO_CONDITION},
|
||||
{"lite_unwatch", byRef(&doLiteUnwatch), Role::ADMIN, NO_CONDITION},
|
||||
{"lite_watch", byRef(&doLiteWatch), Role::ADMIN, NO_CONDITION},
|
||||
{"log_level", byRef(&doLogLevel), Role::ADMIN, NO_CONDITION},
|
||||
{"logrotate", byRef(&doLogRotate), Role::ADMIN, NO_CONDITION},
|
||||
{"manifest", byRef(&doManifest), Role::USER, NO_CONDITION},
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/shamap/SHAMapMissingNode.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/json/Object.h>
|
||||
@@ -205,6 +206,28 @@ callMethod(
|
||||
perfLog.rpcFinish(name, curId);
|
||||
return ret;
|
||||
}
|
||||
catch (SHAMapMissingNode const& e)
|
||||
{
|
||||
perfLog.rpcError(name, curId);
|
||||
|
||||
// On a lite node a missing node is the expected answer for any
|
||||
// object outside the watch set: say so, rather than "internal".
|
||||
if (context.app.config().liteMode())
|
||||
{
|
||||
JLOG(context.j.debug()) << "Lite node asked for an unwatched "
|
||||
<< "object in " << name << ": " << e.what();
|
||||
inject_error(rpcLITE_NOT_WATCHED, result);
|
||||
return rpcLITE_NOT_WATCHED;
|
||||
}
|
||||
|
||||
JLOG(context.j.info()) << "Caught throw: " << e.what();
|
||||
|
||||
if (context.loadType == Resource::feeReferenceRPC)
|
||||
context.loadType = Resource::feeExceptionRPC;
|
||||
|
||||
inject_error(rpcINTERNAL, result);
|
||||
return rpcINTERNAL;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
perfLog.rpcError(name, curId);
|
||||
|
||||
@@ -81,6 +81,12 @@ doLedgerAccept(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerCleaner(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLiteStatus(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLiteUnwatch(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLiteWatch(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerClosed(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerCurrent(RPC::JsonContext&);
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
namespace {
|
||||
|
||||
/** Collect the keys a lite_watch / lite_unwatch request names.
|
||||
|
||||
{
|
||||
"accounts": ["rXXX", ...], // AccountRoot, plus discovery
|
||||
"keylets": ["<64 hex>", ...], // any ledger object by key
|
||||
"lines": [ // trust lines by their parties
|
||||
{"account": "rXXX", "issuer": "rYYY", "currency": "USD"}, ...
|
||||
]
|
||||
}
|
||||
|
||||
A single "account" is accepted as well, as in account_info.
|
||||
*/
|
||||
struct Wanted
|
||||
{
|
||||
std::vector<AccountID> accounts;
|
||||
std::vector<std::pair<uint256, std::string>> keys;
|
||||
};
|
||||
|
||||
std::optional<Json::Value>
|
||||
collect(Json::Value const& params, Wanted& wanted)
|
||||
{
|
||||
auto const account = [&](Json::Value const& v, AccountID& out) {
|
||||
if (!v.isString())
|
||||
return false;
|
||||
auto const id = parseBase58<AccountID>(v.asString());
|
||||
if (!id)
|
||||
return false;
|
||||
out = *id;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (params.isMember(jss::account))
|
||||
{
|
||||
AccountID id;
|
||||
if (!account(params[jss::account], id))
|
||||
return RPC::make_error(rpcACT_MALFORMED);
|
||||
wanted.accounts.push_back(id);
|
||||
}
|
||||
|
||||
if (params.isMember(jss::accounts))
|
||||
{
|
||||
auto const& list = params[jss::accounts];
|
||||
if (!list.isArray())
|
||||
return RPC::expected_field_error(jss::accounts, "array");
|
||||
for (auto const& v : list)
|
||||
{
|
||||
AccountID id;
|
||||
if (!account(v, id))
|
||||
return RPC::make_error(rpcACT_MALFORMED);
|
||||
wanted.accounts.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.isMember("keylets"))
|
||||
{
|
||||
auto const& list = params["keylets"];
|
||||
if (!list.isArray())
|
||||
return RPC::expected_field_error("keylets", "array");
|
||||
for (auto const& v : list)
|
||||
{
|
||||
uint256 key;
|
||||
if (!v.isString() || v.asString().size() != 64 ||
|
||||
!key.parseHex(v.asString()))
|
||||
return RPC::make_param_error(
|
||||
"keylets entries must be 64 hex digits");
|
||||
wanted.keys.emplace_back(key, "keylet");
|
||||
}
|
||||
}
|
||||
|
||||
if (params.isMember(jss::lines))
|
||||
{
|
||||
auto const& list = params[jss::lines];
|
||||
if (!list.isArray())
|
||||
return RPC::expected_field_error(jss::lines, "array");
|
||||
for (auto const& v : list)
|
||||
{
|
||||
AccountID a, issuer;
|
||||
Currency currency;
|
||||
if (!v.isObject() || !account(v[jss::account], a) ||
|
||||
!account(v[jss::issuer], issuer) ||
|
||||
!v[jss::currency].isString() ||
|
||||
!to_currency(currency, v[jss::currency].asString()))
|
||||
return RPC::make_param_error(
|
||||
"lines entries need account, issuer and currency");
|
||||
wanted.keys.emplace_back(
|
||||
keylet::line(a, issuer, currency).key,
|
||||
"line " + toBase58(a) + " " + to_string(currency) + " " +
|
||||
toBase58(issuer));
|
||||
}
|
||||
}
|
||||
|
||||
if (wanted.accounts.empty() && wanted.keys.empty())
|
||||
return RPC::make_param_error(
|
||||
"nothing to watch: give accounts, keylets or lines");
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
notLite()
|
||||
{
|
||||
return RPC::make_error(
|
||||
rpcNOT_SUPPORTED, "This server is not running in [lite_mode].");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Json::Value
|
||||
doLiteWatch(RPC::JsonContext& context)
|
||||
{
|
||||
auto& watch = context.app.getLiteWatch();
|
||||
if (!watch.enabled())
|
||||
return notLite();
|
||||
|
||||
Wanted wanted;
|
||||
if (auto err = collect(context.params, wanted))
|
||||
return *err;
|
||||
|
||||
Json::Value ret{Json::objectValue};
|
||||
Json::Value added{Json::arrayValue};
|
||||
for (auto const& id : wanted.accounts)
|
||||
if (watch.watchAccount(id))
|
||||
added.append(toBase58(id));
|
||||
for (auto const& [key, reason] : wanted.keys)
|
||||
if (watch.watchKey(key, LiteWatch::Tier::configured, reason))
|
||||
added.append(to_string(key));
|
||||
|
||||
ret["added"] = std::move(added);
|
||||
ret["key_count"] = static_cast<Json::UInt>(watch.size());
|
||||
|
||||
// Nothing is fetched retroactively: the new keys are read from the
|
||||
// next validated ledger, which is a few seconds away.
|
||||
if (auto const valid = context.ledgerMaster.getValidatedLedger())
|
||||
ret["effective_from_ledger"] = valid->info().seq + 1;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
doLiteUnwatch(RPC::JsonContext& context)
|
||||
{
|
||||
auto& watch = context.app.getLiteWatch();
|
||||
if (!watch.enabled())
|
||||
return notLite();
|
||||
|
||||
Wanted wanted;
|
||||
if (auto err = collect(context.params, wanted))
|
||||
return *err;
|
||||
|
||||
Json::Value ret{Json::objectValue};
|
||||
Json::Value removed{Json::arrayValue};
|
||||
for (auto const& id : wanted.accounts)
|
||||
if (watch.unwatchAccount(id))
|
||||
removed.append(toBase58(id));
|
||||
for (auto const& [key, reason] : wanted.keys)
|
||||
if (watch.unwatchKey(key))
|
||||
removed.append(to_string(key));
|
||||
|
||||
ret["removed"] = std::move(removed);
|
||||
ret["key_count"] = static_cast<Json::UInt>(watch.size());
|
||||
return ret;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
doLiteStatus(RPC::JsonContext& context)
|
||||
{
|
||||
auto& watch = context.app.getLiteWatch();
|
||||
auto ret = watch.getJson();
|
||||
|
||||
if (!context.params.isMember("verbose") ||
|
||||
!context.params["verbose"].asBool())
|
||||
ret.removeMember("keys");
|
||||
|
||||
if (auto const valid = context.ledgerMaster.getValidatedLedger())
|
||||
{
|
||||
ret[jss::ledger_index] = valid->info().seq;
|
||||
ret[jss::ledger_hash] = to_string(valid->info().hash);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/main/Tuning.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/shamap/NodeFamily.h>
|
||||
#include <sstream>
|
||||
|
||||
@@ -69,6 +70,16 @@ NodeFamily::reset()
|
||||
void
|
||||
NodeFamily::missingNodeAcquireBySeq(std::uint32_t seq, uint256 const& nodeHash)
|
||||
{
|
||||
// A lite ledger is missing almost everything by design, and is as
|
||||
// complete as it will get: acquiring it again would fetch the same
|
||||
// paths. Reads of unwatched keys surface as SHAMapMissingNode instead.
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
JLOG(j_.trace()) << "Lite mode: unwatched node " << nodeHash << " in "
|
||||
<< seq;
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(j_.error()) << "Missing node in " << seq;
|
||||
std::unique_lock<std::mutex> lock(maxSeqMutex_);
|
||||
if (maxSeq_ == 0)
|
||||
|
||||
Reference in new issue
Block a user