mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-24 07:40:53 +00:00
Compare commits
6 Commits
dangell7/m
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4c1359921 | ||
|
|
a88ba66fce | ||
|
|
4bd1d1ca2f | ||
|
|
0cce5a06d9 | ||
|
|
32a9cc4038 | ||
|
|
587505ef18 |
@@ -39,6 +39,35 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace base64 {
|
||||
|
||||
/**
|
||||
* Returns the maximum number of characters needed to base64-encode @p nBytes bytes.
|
||||
*
|
||||
* @param nBytes Number of input bytes.
|
||||
* @return Size of the encoded string, including padding.
|
||||
*/
|
||||
constexpr std::size_t
|
||||
encodedSize(std::size_t const nBytes)
|
||||
{
|
||||
return 4 * ((nBytes + 2) / 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes a base64 string of @p numChars characters
|
||||
* decodes to.
|
||||
*
|
||||
* @param numChars Number of base64 characters.
|
||||
* @return Upper bound on the number of decoded bytes.
|
||||
*/
|
||||
constexpr std::size_t
|
||||
decodedSize(std::size_t const numChars)
|
||||
{
|
||||
return ((numChars / 4) * 3) + 2;
|
||||
}
|
||||
|
||||
} // namespace base64
|
||||
|
||||
std::string
|
||||
base64Encode(std::uint8_t const* data, std::size_t len);
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
@@ -35,12 +41,15 @@ namespace xrpl {
|
||||
dynamically generates the signatureless form when it needs to verify
|
||||
the signature.
|
||||
|
||||
An instance of ManifestCache stores, for each trusted validator, (a) its
|
||||
An instance of ManifestCache stores, for each known validator, (a) its
|
||||
master public key, and (b) the most senior of all valid manifests it has
|
||||
seen for that validator, if any. On startup, the [validator_token] config
|
||||
entry (which contains the manifest for this validator) is decoded and
|
||||
added to the manifest cache. Other manifests are added as "gossip"
|
||||
received from xrpld peers.
|
||||
received from xrpld peers, including ones for validators this node does not
|
||||
trust. Manifests for untrusted validators are capped (kMaxUntrustedCount)
|
||||
so peer gossip cannot grow the cache without bound; trusted validators are
|
||||
not capped. Entries are never evicted, so a stored revocation is permanent.
|
||||
|
||||
When an ephemeral key is compromised, a new signing key pair is created,
|
||||
along with a new manifest vouching for it (with a higher sequence number),
|
||||
@@ -132,6 +141,53 @@ struct Manifest
|
||||
std::string
|
||||
to_string(Manifest const& m);
|
||||
|
||||
/**
|
||||
*Largest a valid manifest can be, in decoded bytes.
|
||||
*
|
||||
* A manifest has a fixed set of fields. Each is serialized as a field header
|
||||
* (1-2 bytes), an optional length prefix (1 byte for these sizes), and the
|
||||
* field body. Taking every field at its largest gives the maximum below, so
|
||||
* anything larger cannot be a valid manifest.
|
||||
*
|
||||
* Field header + length + body = bytes
|
||||
* sfVersion (U16) 2 0 2 4
|
||||
* sfSequence (U32) 1 0 4 5
|
||||
* sfPublicKey (33) 1 1 33 35
|
||||
* sfSigningPubKey (33) 1 1 33 35
|
||||
* sfSignature (72) 1 1 72 74
|
||||
* sfMasterSignature (72) 2 1 72 75
|
||||
* sfDomain (128) 1 1 128 130
|
||||
* -----
|
||||
* 358
|
||||
*/
|
||||
constexpr std::size_t kMaxManifestBytes = 358;
|
||||
|
||||
/**
|
||||
* Largest a valid manifest can be, in base64 characters.
|
||||
*
|
||||
* base64 encodes 3 bytes as 4 characters, so this is the encoded form of
|
||||
* @ref kMaxManifestBytes. Callers that receive a base64 manifest should
|
||||
* reject anything longer than this before decoding, to avoid allocating
|
||||
* memory for an oversized input.
|
||||
*/
|
||||
constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes);
|
||||
|
||||
/** Maximum number of manifests carried in a single TMManifests message.
|
||||
|
||||
Outbound, the TMManifests message sent to a peer includes every trusted
|
||||
manifest and fills the rest of this budget with untrusted gossip, so it
|
||||
never exceeds this size. Inbound, trusted manifests are always processed
|
||||
and untrusted ones are processed up to this many, so a peer sending its
|
||||
whole cache cannot force unbounded work.
|
||||
|
||||
The trusted set is tiny relative to this bound, so trusted manifests are
|
||||
not dropped in practice. This is a transitional per-message cap; the cache
|
||||
already bounds untrusted manifests (see kMaxUntrustedCount), so it is no
|
||||
longer needed once the network has upgraded past nodes that send their
|
||||
whole cache in one message.
|
||||
*/
|
||||
constexpr std::size_t kMaxManifestsPerMessage = 200;
|
||||
|
||||
/** Constructs Manifest from serialized string
|
||||
|
||||
@param s Serialized manifest string
|
||||
@@ -206,7 +262,10 @@ enum class ManifestDisposition {
|
||||
BadEphemeralKey,
|
||||
|
||||
/// Timely, but invalid signature
|
||||
Invalid
|
||||
Invalid,
|
||||
|
||||
/// Unlisted and limit reached
|
||||
UntrustedCapacity
|
||||
};
|
||||
|
||||
inline std::string
|
||||
@@ -224,11 +283,25 @@ to_string(ManifestDisposition m)
|
||||
return "badEphemeralKey";
|
||||
case ManifestDisposition::Invalid:
|
||||
return "invalid";
|
||||
case ManifestDisposition::UntrustedCapacity:
|
||||
return "untrustedCapacity";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a manifest counts against the 'untrusted' cache cap.
|
||||
*
|
||||
* Passed to `ManifestCache::applyManifest` with no default, so every caller
|
||||
* must choose. `Capped` is the safe, flood-resistant value; only listed or
|
||||
* configured keys should use `Uncapped`.
|
||||
*/
|
||||
enum class ManifestRateLimitCapPolicy : std::uint8_t {
|
||||
Capped, ///< Subject to the untrusted cap (unlisted peer gossip)
|
||||
Uncapped ///< Bypasses the cap (listed/trusted or config manifests)
|
||||
};
|
||||
|
||||
class DatabaseCon;
|
||||
|
||||
/** Remembers manifests with the highest sequence number. */
|
||||
@@ -246,6 +319,38 @@ private:
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
/**
|
||||
* Master keys of cached manifests for validators this node does not list.
|
||||
*
|
||||
* One entry per capped key in `map_`; its size enforces the cap below.
|
||||
* A key is added when first cached under `Capped` and removed when it
|
||||
* becomes listed (see `promoteToTrusted`) or an `Uncapped` update arrives,
|
||||
* never re-added on de-listing. Uncapped keys are not tracked here.
|
||||
*/
|
||||
hash_set<PublicKey> untrustedKeys_;
|
||||
|
||||
/**
|
||||
* Maximum number of untrusted master keys kept in the cache.
|
||||
*
|
||||
* Once reached, a manifest for a brand-new unlisted key is rejected.
|
||||
*/
|
||||
static constexpr std::size_t kMaxUntrustedCount = 100;
|
||||
|
||||
/**
|
||||
* Running count of manifests rejected because the untrusted cap was full.
|
||||
*
|
||||
* Drives throttled logging (see `kUntrustedRejectCount`). Atomic because
|
||||
* `applyManifest` may run concurrently.
|
||||
*/
|
||||
std::atomic<std::uint64_t> untrustedRejectCount_{0};
|
||||
|
||||
/**
|
||||
* Number of cap rejections between summary warnings.
|
||||
*
|
||||
* @see untrustedRejectCount_
|
||||
*/
|
||||
static constexpr std::uint64_t kUntrustedRejectCount = 10000;
|
||||
|
||||
public:
|
||||
explicit ManifestCache(beast::Journal j = beast::Journal(beast::Journal::getNullSink())) : j_(j)
|
||||
{
|
||||
@@ -321,17 +426,44 @@ public:
|
||||
|
||||
/** Add manifest to cache.
|
||||
|
||||
A brand-new unlisted key is rejected once the untrusted cap is full;
|
||||
updates to a cached key and `Uncapped` manifests bypass the cap. The
|
||||
caller decides `cap` before calling so the cache lock is not held while
|
||||
consulting the validator list, which would risk a lock-ordering deadlock.
|
||||
|
||||
@param m Manifest to add
|
||||
|
||||
@return `ManifestDisposition::accepted` if successful, or
|
||||
`stale` or `invalid` otherwise
|
||||
@param cap `Uncapped` skips the untrusted cap; use it for keys that are
|
||||
listed, configured, or loaded from the DB. Note `Uncapped` does
|
||||
not assert the key is currently trusted (a DB entry may predate a
|
||||
de-listing). Callers must state this explicitly so a manifest is
|
||||
never left uncapped by omission.
|
||||
|
||||
@return `Accepted` if stored, `Stale` if superseded, `Invalid`/
|
||||
`BadEphemeralKey` if malformed, or `UntrustedCapacity` if the
|
||||
untrusted cap is full.
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
ManifestDisposition
|
||||
applyManifest(Manifest m);
|
||||
applyManifest(Manifest m, ManifestRateLimitCapPolicy cap);
|
||||
|
||||
/**
|
||||
* Stop counting a master key against the untrusted cap.
|
||||
*
|
||||
* Called when a cached untrusted key becomes listed, freeing its slot.
|
||||
* Idempotent and a no-op for keys that were never counted.
|
||||
*
|
||||
* @param pk Master public key that is now listed/trusted
|
||||
*
|
||||
* @par Thread Safety
|
||||
*
|
||||
* May be called concurrently
|
||||
*/
|
||||
void
|
||||
promoteToTrusted(PublicKey const& pk);
|
||||
|
||||
/** Populate manifest cache with manifests in database and config.
|
||||
|
||||
|
||||
@@ -76,20 +76,6 @@ getInverse()
|
||||
return &kTab[0];
|
||||
}
|
||||
|
||||
/// Returns max chars needed to encode a base64 string
|
||||
constexpr std::size_t
|
||||
encodedSize(std::size_t n)
|
||||
{
|
||||
return 4 * ((n + 2) / 3);
|
||||
}
|
||||
|
||||
/// Returns max bytes needed to decode a base64 string
|
||||
constexpr std::size_t
|
||||
decodedSize(std::size_t n)
|
||||
{
|
||||
return ((n / 4) * 3) + 2;
|
||||
}
|
||||
|
||||
/** Encode a series of octets as a padded, base64 string.
|
||||
|
||||
The resulting string will not be null terminated.
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.2.0"
|
||||
char const* const versionString = "3.2.1"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ deserializeManifest(Slice s, beast::Journal journal)
|
||||
if (s.empty())
|
||||
return std::nullopt;
|
||||
|
||||
// A valid manifest has a fixed maximum size, so reject anything larger
|
||||
// before parsing it.
|
||||
if (s.size() > kMaxManifestBytes)
|
||||
return std::nullopt;
|
||||
|
||||
static SOTemplate const kManifestFormat{
|
||||
// A manifest must include:
|
||||
// - the master public key
|
||||
@@ -377,16 +382,20 @@ ManifestCache::revoked(PublicKey const& pk) const
|
||||
}
|
||||
|
||||
ManifestDisposition
|
||||
ManifestCache::applyManifest(Manifest m)
|
||||
ManifestCache::applyManifest(Manifest m, ManifestRateLimitCapPolicy const cap)
|
||||
{
|
||||
bool const uncapped = cap == ManifestRateLimitCapPolicy::Uncapped;
|
||||
|
||||
// The signature is checked only on the first `prewriteCheck` run (under the
|
||||
// read lock). It is expensive, so `checkSignature` is cleared the first
|
||||
// time it is read; the second run (under the write lock) skips it.
|
||||
bool checkSignature = true;
|
||||
|
||||
// Check the manifest against the conditions that do not require a
|
||||
// `unique_lock` (write lock) on the `mutex_`. Since the signature can be
|
||||
// relatively expensive, the `checkSignature` parameter determines if the
|
||||
// signature should be checked. Since `prewriteCheck` is run twice (see
|
||||
// comment below), `checkSignature` only needs to be set to true on the
|
||||
// first run.
|
||||
auto prewriteCheck = [this, &m](auto const& iter, bool checkSignature, auto const& lock)
|
||||
-> std::optional<ManifestDisposition> {
|
||||
// `unique_lock` (write lock) on the `mutex_`.
|
||||
auto prewriteCheck = [this, &m, &checkSignature](
|
||||
auto const& iter,
|
||||
auto const& lock) -> std::optional<ManifestDisposition> {
|
||||
XRPL_ASSERT(lock.owns_lock(), "xrpl::ManifestCache::applyManifest::prewriteCheck : locked");
|
||||
(void)lock; // not used. parameter is present to ensure the mutex is
|
||||
// locked when the lambda is called.
|
||||
@@ -401,11 +410,15 @@ ManifestCache::applyManifest(Manifest m)
|
||||
return ManifestDisposition::Stale;
|
||||
}
|
||||
|
||||
if (checkSignature && !m.verify())
|
||||
if (checkSignature)
|
||||
{
|
||||
if (auto stream = j_.warn())
|
||||
logMftAct(stream, "Invalid", m.masterKey, m.sequence);
|
||||
return ManifestDisposition::Invalid;
|
||||
checkSignature = false;
|
||||
if (!m.verify())
|
||||
{
|
||||
if (auto stream = j_.warn())
|
||||
logMftAct(stream, "Invalid", m.masterKey, m.sequence);
|
||||
return ManifestDisposition::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
// If the master key associated with a manifest is or might be
|
||||
@@ -465,14 +478,51 @@ ManifestCache::applyManifest(Manifest m)
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// Reject a brand-new manifest for an unlisted key once the untrusted cap
|
||||
// is full. Updates to a cached key and uncapped manifests always pass.
|
||||
// Called under both the read and write lock, since the cap can be reached
|
||||
// between the two. The lock param enforces that.
|
||||
auto atUntrustedCap = [this, &m, uncapped](auto const& iter, auto const& lock) {
|
||||
XRPL_ASSERT(
|
||||
lock.owns_lock(), "xrpl::ManifestCache::applyManifest::atUntrustedCap : locked");
|
||||
(void)lock; // not used. parameter is present to ensure the mutex is
|
||||
// locked when the lambda is called.
|
||||
if (iter == map_.end() && !uncapped && untrustedKeys_.size() >= kMaxUntrustedCount)
|
||||
{
|
||||
// Log each rejection at debug, but warn only once per interval so a
|
||||
// flood does not fill the log.
|
||||
if (auto stream = j_.debug())
|
||||
logMftAct(stream, "UntrustedCapacity", m.masterKey, m.sequence);
|
||||
if (auto const n = untrustedRejectCount_.fetch_add(1) + 1;
|
||||
n % kUntrustedRejectCount == 0)
|
||||
{
|
||||
JLOG(j_.warn()) << "Untrusted manifest cap reached; " << n
|
||||
<< " manifests rejected so far";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
{
|
||||
std::shared_lock const sl{mutex_};
|
||||
if (auto d = prewriteCheck(map_.find(m.masterKey), /*checkSig*/ true, sl))
|
||||
auto const iter = map_.find(m.masterKey);
|
||||
|
||||
if (atUntrustedCap(iter, sl))
|
||||
return ManifestDisposition::UntrustedCapacity;
|
||||
|
||||
if (auto d = prewriteCheck(iter, sl); d.has_value())
|
||||
return *d;
|
||||
}
|
||||
|
||||
std::unique_lock const sl{mutex_};
|
||||
auto const iter = map_.find(m.masterKey);
|
||||
|
||||
// Re-check the cap under the write lock: the cache may have grown while the
|
||||
// read lock above was released.
|
||||
if (atUntrustedCap(iter, sl))
|
||||
return ManifestDisposition::UntrustedCapacity;
|
||||
|
||||
// Since we released the previously held read lock, it's possible that the
|
||||
// collections have been written to. This means we need to run
|
||||
// `prewriteCheck` again. This re-does work, but `prewriteCheck` is
|
||||
@@ -482,7 +532,7 @@ ManifestCache::applyManifest(Manifest m)
|
||||
// doesn't need to happen again (signature checks are somewhat expensive).
|
||||
// Note: It's a mistake to use an upgradable lock. This is a recipe for
|
||||
// deadlock.
|
||||
if (auto d = prewriteCheck(iter, /*checkSig*/ false, sl))
|
||||
if (auto d = prewriteCheck(iter, sl); d.has_value())
|
||||
return *d;
|
||||
|
||||
bool const revoked = m.revoked();
|
||||
@@ -501,6 +551,12 @@ ManifestCache::applyManifest(Manifest m)
|
||||
}
|
||||
|
||||
auto masterKey = m.masterKey;
|
||||
|
||||
// Count this key against the untrusted cap. Uncapped keys (listed,
|
||||
// configured, or DB-loaded) are not tracked.
|
||||
if (!uncapped)
|
||||
untrustedKeys_.insert(masterKey);
|
||||
|
||||
map_.emplace(std::move(masterKey), std::move(m));
|
||||
|
||||
// Something has changed. Keep track of it.
|
||||
@@ -514,6 +570,11 @@ ManifestCache::applyManifest(Manifest m)
|
||||
if (auto stream = j_.info())
|
||||
logMftAct(stream, "AcceptedUpdate", m.masterKey, m.sequence, iter->second.sequence);
|
||||
|
||||
// If this key was counted against the cap but now arrives uncapped, free
|
||||
// its slot without waiting for promoteToTrusted.
|
||||
if (uncapped)
|
||||
untrustedKeys_.erase(m.masterKey);
|
||||
|
||||
signingToMasterKeys_.erase(
|
||||
*iter->second.signingKey); // NOLINT(bugprone-unchecked-optional-access) prewriteCheck
|
||||
// ensures old manifest is not revoked
|
||||
@@ -521,8 +582,8 @@ ManifestCache::applyManifest(Manifest m)
|
||||
if (!revoked)
|
||||
{
|
||||
signingToMasterKeys_.emplace(
|
||||
*m.signingKey, m.masterKey); // NOLINT(bugprone-unchecked-optional-access) non-revoked
|
||||
// manifest always has signingKey
|
||||
*m.signingKey, m.masterKey); // NOLINT(bugprone-unchecked-optional-access)
|
||||
// non-revoked manifest always has signingKey
|
||||
}
|
||||
|
||||
iter->second = std::move(m);
|
||||
@@ -533,6 +594,16 @@ ManifestCache::applyManifest(Manifest m)
|
||||
return ManifestDisposition::Accepted;
|
||||
}
|
||||
|
||||
void
|
||||
ManifestCache::promoteToTrusted(PublicKey const& pk)
|
||||
{
|
||||
// Frees the key's untrusted slot; a no-op (and idempotent) if the key was
|
||||
// never counted. Not re-added on de-listing, so list/de-list cannot grow
|
||||
// the count.
|
||||
std::unique_lock const sl{mutex_};
|
||||
untrustedKeys_.erase(pk);
|
||||
}
|
||||
|
||||
void
|
||||
ManifestCache::load(DatabaseCon& dbCon, std::string const& dbTable)
|
||||
{
|
||||
@@ -563,7 +634,8 @@ ManifestCache::load(
|
||||
JLOG(j_.warn()) << "Configured manifest revokes public key";
|
||||
}
|
||||
|
||||
if (applyManifest(std::move(*mo)) == ManifestDisposition::Invalid)
|
||||
if (applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped) ==
|
||||
ManifestDisposition::Invalid)
|
||||
{
|
||||
JLOG(j_.error()) << "Manifest in config was rejected";
|
||||
return false;
|
||||
@@ -585,7 +657,9 @@ ManifestCache::load(
|
||||
|
||||
auto mo = deserializeManifest(base64Decode(revocationStr));
|
||||
|
||||
if (!mo || !mo->revoked() || applyManifest(std::move(*mo)) == ManifestDisposition::Invalid)
|
||||
if (!mo || !mo->revoked() ||
|
||||
applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped) ==
|
||||
ManifestDisposition::Invalid)
|
||||
{
|
||||
JLOG(j_.error()) << "Invalid validator key revocation in config";
|
||||
return false;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <soci/use.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -75,7 +76,9 @@ getManifests(
|
||||
continue;
|
||||
}
|
||||
|
||||
cache.applyManifest(std::move(*mo));
|
||||
// Only trusted manifests are persisted (see saveManifests), so
|
||||
// anything loaded from the DB bypasses the untrusted cap.
|
||||
cache.applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -105,19 +108,27 @@ saveManifests(
|
||||
{
|
||||
soci::transaction tr(session);
|
||||
session << "DELETE FROM " << dbTable;
|
||||
// Count skipped untrusted manifests and log one summary afterwards, since
|
||||
// the cache can hold many and per-entry logging would flood at shutdown.
|
||||
std::size_t skipped = 0;
|
||||
for (auto const& v : map)
|
||||
{
|
||||
// Save all revocation manifests,
|
||||
// but only save trusted non-revocation manifests.
|
||||
if (!v.second.revoked() && !isTrusted(v.second.masterKey))
|
||||
// Persist only trusted keys. Untrusted gossip is left out so a flood
|
||||
// cannot survive a restart on disk.
|
||||
if (!isTrusted(v.second.masterKey))
|
||||
{
|
||||
JLOG(j.info()) << "Untrusted manifest in cache not saved to db";
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
saveManifest(session, dbTable, v.second.serialized);
|
||||
}
|
||||
tr.commit();
|
||||
|
||||
if (skipped != 0)
|
||||
{
|
||||
JLOG(j.info()) << skipped << " untrusted manifest(s) in cache not saved to db";
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -399,7 +399,8 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::Accepted ==
|
||||
cache.applyManifest(
|
||||
makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0)));
|
||||
makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0),
|
||||
ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk);
|
||||
|
||||
@@ -411,7 +412,8 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::Accepted ==
|
||||
cache.applyManifest(
|
||||
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1)));
|
||||
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1),
|
||||
ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
||||
@@ -421,7 +423,8 @@ public:
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::BadEphemeralKey ==
|
||||
cache.applyManifest(
|
||||
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2)));
|
||||
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2),
|
||||
ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
||||
@@ -431,7 +434,8 @@ public:
|
||||
// key from a revoked master public key
|
||||
BEAST_EXPECT(
|
||||
ManifestDisposition::Accepted ==
|
||||
cache.applyManifest(makeRevocation(sk, KeyType::Ed25519)));
|
||||
cache.applyManifest(
|
||||
makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCapPolicy::Capped));
|
||||
BEAST_EXPECT(cache.revoked(pk));
|
||||
BEAST_EXPECT(cache.getSigningKey(pk) == pk);
|
||||
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
||||
@@ -902,39 +906,69 @@ public:
|
||||
// applyManifest should accept new manifests with
|
||||
// higher sequence numbers
|
||||
auto const seq0 = cache.sequence();
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(cache.sequence() > seq0);
|
||||
|
||||
auto const seq1 = cache.sequence();
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(cache.sequence() == seq1);
|
||||
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA2)) == ManifestDisposition::BadEphemeralKey);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA2), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::BadEphemeralKey);
|
||||
|
||||
// applyManifest should accept manifests with max sequence numbers
|
||||
// that revoke the master public key
|
||||
BEAST_EXPECT(!cache.revoked(pkA));
|
||||
BEAST_EXPECT(sAMax.revoked());
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sAMax), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sAMax), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(cache.revoked(pkA));
|
||||
|
||||
// applyManifest should reject manifests with invalid signatures
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sB0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sB0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Stale);
|
||||
BEAST_EXPECT(!deserializeManifest(fake));
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sB1)) == ManifestDisposition::Invalid);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sB2)) == ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sB1), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Invalid);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sB2), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
|
||||
auto const sC0 = makeManifest(
|
||||
kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47);
|
||||
BEAST_EXPECT(cache.applyManifest(clone(sC0)) == ManifestDisposition::BadMasterKey);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(clone(sC0), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::BadMasterKey);
|
||||
}
|
||||
|
||||
testLoadStore(cache);
|
||||
|
||||
@@ -277,8 +277,10 @@ private:
|
||||
trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers));
|
||||
BEAST_EXPECT(trustedKeys->listed(localSigningPublicOuter));
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
manifests.applyManifest(*deserializeManifest(cfgManifest));
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
manifests.applyManifest(
|
||||
*deserializeManifest(cfgManifest), ManifestRateLimitCapPolicy::Capped);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(
|
||||
trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers));
|
||||
|
||||
@@ -368,8 +370,10 @@ private:
|
||||
app.config().legacy("database_path"),
|
||||
env.journal);
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
manifests.applyManifest(*deserializeManifest(cfgManifest));
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
manifests.applyManifest(
|
||||
*deserializeManifest(cfgManifest), ManifestRateLimitCapPolicy::Capped);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
BEAST_EXPECT(trustedKeys->load(localSigningPublicOuter, cfgKeys, emptyCfgPublishers));
|
||||
|
||||
@@ -454,13 +458,16 @@ private:
|
||||
auto const pubRevokedSigning = randomKeyPair(KeyType::Secp256k1);
|
||||
// make this manifest revoked (seq num = max)
|
||||
// -- thus should not be loaded
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
pubManifests.applyManifest(*deserializeManifest(makeManifestString(
|
||||
pubRevokedPublic,
|
||||
pubRevokedSecret,
|
||||
pubRevokedSigning.first,
|
||||
pubRevokedSigning.second,
|
||||
std::numeric_limits<std::uint32_t>::max())));
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
pubManifests.applyManifest(
|
||||
*deserializeManifest(makeManifestString(
|
||||
pubRevokedPublic,
|
||||
pubRevokedSecret,
|
||||
pubRevokedSigning.first,
|
||||
pubRevokedSigning.second,
|
||||
std::numeric_limits<std::uint32_t>::max())),
|
||||
ManifestRateLimitCapPolicy::Capped);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
// these two are not revoked (and not in the manifest cache at all.)
|
||||
auto legitKey1 = randomMasterKey();
|
||||
@@ -493,13 +500,16 @@ private:
|
||||
auto const pubRevokedSigning = randomKeyPair(KeyType::Secp256k1);
|
||||
// make this manifest revoked (seq num = max)
|
||||
// -- thus should not be loaded
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
pubManifests.applyManifest(*deserializeManifest(makeManifestString(
|
||||
pubRevokedPublic,
|
||||
pubRevokedSecret,
|
||||
pubRevokedSigning.first,
|
||||
pubRevokedSigning.second,
|
||||
std::numeric_limits<std::uint32_t>::max())));
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
pubManifests.applyManifest(
|
||||
*deserializeManifest(makeManifestString(
|
||||
pubRevokedPublic,
|
||||
pubRevokedSecret,
|
||||
pubRevokedSigning.first,
|
||||
pubRevokedSigning.second,
|
||||
std::numeric_limits<std::uint32_t>::max())),
|
||||
ManifestRateLimitCapPolicy::Capped);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
// this one is not revoked (and not in the manifest cache at all.)
|
||||
auto legitKey = randomMasterKey();
|
||||
@@ -1164,7 +1174,8 @@ private:
|
||||
|
||||
BEAST_EXPECT(
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
manifestsOuter.applyManifest(std::move(*m1)) == ManifestDisposition::Accepted);
|
||||
manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
||||
BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic));
|
||||
BEAST_EXPECT(trustedKeysOuter->listed(signingPublic1));
|
||||
@@ -1178,7 +1189,8 @@ private:
|
||||
masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2));
|
||||
BEAST_EXPECT(
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
manifestsOuter.applyManifest(std::move(*m2)) == ManifestDisposition::Accepted);
|
||||
manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
BEAST_EXPECT(trustedKeysOuter->listed(masterPublic));
|
||||
BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic));
|
||||
BEAST_EXPECT(trustedKeysOuter->listed(signingPublic2));
|
||||
@@ -1195,7 +1207,8 @@ private:
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
BEAST_EXPECT(max->revoked());
|
||||
BEAST_EXPECT(
|
||||
manifestsOuter.applyManifest(std::move(*max)) == ManifestDisposition::Accepted);
|
||||
manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCapPolicy::Capped) ==
|
||||
ManifestDisposition::Accepted);
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
|
||||
BEAST_EXPECT(manifestsOuter.getSigningKey(masterPublic) == masterPublic);
|
||||
@@ -2612,7 +2625,9 @@ private:
|
||||
auto threshold = listThreshold > 0 ? std::optional(listThreshold) : std::nullopt;
|
||||
if (self)
|
||||
{
|
||||
valManifests.applyManifest(*deserializeManifest(base64Decode(self->manifest)));
|
||||
valManifests.applyManifest(
|
||||
*deserializeManifest(base64Decode(self->manifest)),
|
||||
ManifestRateLimitCapPolicy::Capped);
|
||||
BEAST_EXPECT(
|
||||
result->load(self->signingPublic, emptyCfgKeys, cfgPublishers, threshold));
|
||||
}
|
||||
|
||||
@@ -1065,6 +1065,8 @@ ValidatorList::updatePublisherList(
|
||||
{
|
||||
// Increment list count for added keys
|
||||
++keyListings_[*iNew];
|
||||
// Key is now listed: free its untrusted slot if it had one.
|
||||
validatorManifests_.promoteToTrusted(*iNew);
|
||||
++iNew;
|
||||
}
|
||||
else if (iNew == publisherList.end() || (iOld != oldList.end() && *iOld < *iNew))
|
||||
@@ -1103,7 +1105,8 @@ ValidatorList::updatePublisherList(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto const r = validatorManifests_.applyManifest(std::move(*m));
|
||||
if (auto const r = validatorManifests_.applyManifest(
|
||||
std::move(*m), ManifestRateLimitCapPolicy::Uncapped);
|
||||
r == ManifestDisposition::Invalid)
|
||||
{
|
||||
JLOG(j_.warn()) << "List for " << strHex(pubKey)
|
||||
@@ -1127,6 +1130,15 @@ ValidatorList::applyList(
|
||||
|
||||
json::Value list;
|
||||
auto const& manifest = localManifest ? *localManifest : globalManifest;
|
||||
// Reject an oversized manifest before decoding it, so we do not allocate
|
||||
// memory for an input that cannot be a valid manifest. deserializeManifest
|
||||
// also enforces the decoded-byte limit, but checking here avoids the
|
||||
// base64 decode entirely.
|
||||
if (manifest.size() > kMaxManifestBase64)
|
||||
{
|
||||
JLOG(j_.warn()) << "UNL manifest exceeds maximum size";
|
||||
return PublisherListStats{ListDisposition::Invalid};
|
||||
}
|
||||
auto m = deserializeManifest(base64Decode(manifest));
|
||||
if (!m)
|
||||
{
|
||||
@@ -1348,7 +1360,10 @@ ValidatorList::verify(
|
||||
PublicKey masterPubKey = manifest.masterKey;
|
||||
auto const revoked = manifest.revoked();
|
||||
|
||||
auto const result = publisherManifests_.applyManifest(std::move(manifest));
|
||||
// Publisher keys are configured/trusted (checked above), so bypass the
|
||||
// untrusted cap.
|
||||
auto const result = publisherManifests_.applyManifest(
|
||||
std::move(manifest), ManifestRateLimitCapPolicy::Uncapped);
|
||||
|
||||
if (revoked && result == ManifestDisposition::Accepted)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -13,6 +14,13 @@ namespace xrpl {
|
||||
|
||||
constexpr std::size_t kMaximumMessageSize = megabytes(64);
|
||||
|
||||
// Upper bound on the wire size of a TMManifests message: kMaxManifestsPerMessage entries
|
||||
// of at most kMaxManifestBytes each, plus a small allowance for protobuf
|
||||
// framing per entry.
|
||||
constexpr std::size_t kManifestFramingBytes = 8;
|
||||
constexpr std::size_t kMaximumManifestsMessageSize =
|
||||
kMaxManifestsPerMessage * (kMaxManifestBytes + kManifestFramingBytes);
|
||||
|
||||
// VFALCO NOTE If we forward declare Message and write out shared_ptr
|
||||
// instead of using the in-class type alias, we can remove the
|
||||
// entire ripple.pb.h from the main headers.
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/resource/ResourceManager.h>
|
||||
#include <xrpl/server/Handoff.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
@@ -661,25 +662,53 @@ OverlayImpl::onManifests(
|
||||
std::shared_ptr<protocol::TMManifests> const& m,
|
||||
std::shared_ptr<PeerImp> const& from)
|
||||
{
|
||||
auto const n = m->list_size();
|
||||
auto const& journal = from->pJournal();
|
||||
|
||||
// Process every trusted manifest, but stop processing untrusted ones once
|
||||
// kMaxManifestsPerMessage of them have been handled, so the work stays bounded.
|
||||
auto const total = static_cast<std::size_t>(m->list_size());
|
||||
std::size_t untrusted = 0;
|
||||
bool skippedUntrusted = false;
|
||||
|
||||
protocol::TMManifests relay;
|
||||
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
for (std::size_t i = 0; i < total; ++i)
|
||||
{
|
||||
auto& s = m->list().Get(i).stobject();
|
||||
|
||||
if (auto mo = deserializeManifest(s))
|
||||
{
|
||||
auto const serialized = mo->serialized;
|
||||
// Resolve trust before applyManifest takes the manifest-cache
|
||||
// lock: listed() takes the validator-list lock, so ordering it
|
||||
// first avoids holding the two locks in opposite orders.
|
||||
bool const isTrusted = app_.getValidators().listed(mo->masterKey);
|
||||
|
||||
auto const result = app_.getValidatorManifests().applyManifest(std::move(*mo));
|
||||
// Bound untrusted work: process at most kMaxManifestsPerMessage
|
||||
// untrusted manifests, but never skip a trusted one. Trusted
|
||||
// manifests are not counted against the cap.
|
||||
if (!isTrusted)
|
||||
{
|
||||
if (untrusted >= kMaxManifestsPerMessage)
|
||||
{
|
||||
skippedUntrusted = true;
|
||||
continue;
|
||||
}
|
||||
++untrusted;
|
||||
}
|
||||
// Updates to a known key are relayed even when untrusted. Use
|
||||
// getSequence, not getManifest, to avoid copying the cached payload
|
||||
// on this hot path.
|
||||
bool const isKnown =
|
||||
app_.getValidatorManifests().getSequence(mo->masterKey).has_value();
|
||||
|
||||
auto const result = app_.getValidatorManifests().applyManifest(
|
||||
std::move(*mo),
|
||||
isTrusted ? ManifestRateLimitCapPolicy::Uncapped
|
||||
: ManifestRateLimitCapPolicy::Capped);
|
||||
|
||||
if (result == ManifestDisposition::Accepted)
|
||||
{
|
||||
relay.add_list()->set_stobject(s);
|
||||
|
||||
// N.B.: this is important; the applyManifest call above moves
|
||||
// the loaded Manifest out of the optional so we need to
|
||||
// reload it here.
|
||||
@@ -691,10 +720,19 @@ OverlayImpl::onManifests(
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access) assert above
|
||||
app_.getOPs().pubManifest(*mo);
|
||||
|
||||
if (app_.getValidators().listed(mo->masterKey))
|
||||
// Relay only trusted manifests or updates to known keys, so
|
||||
// untrusted gossip for a brand-new key cannot be amplified.
|
||||
// Persist to the wallet DB only for trusted keys, so untrusted
|
||||
// gossip never survives a restart.
|
||||
if (isTrusted || isKnown)
|
||||
{
|
||||
auto db = app_.getWalletDB().checkoutDb();
|
||||
addValidatorManifest(*db, serialized);
|
||||
relay.add_list()->set_stobject(s);
|
||||
|
||||
if (isTrusted)
|
||||
{
|
||||
auto db = app_.getWalletDB().checkoutDb();
|
||||
addValidatorManifest(*db, serialized);
|
||||
}
|
||||
}
|
||||
// NOLINTEND(bugprone-unchecked-optional-access)
|
||||
}
|
||||
@@ -706,6 +744,18 @@ OverlayImpl::onManifests(
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedUntrusted)
|
||||
{
|
||||
// The sender exceeded the untrusted per-message cap. Charge it (once,
|
||||
// here) so a flood of untrusted manifests is penalized, while an honest
|
||||
// message of trusted manifests never is.
|
||||
from->charge(Resource::kFeeMalformedRequest, "too many untrusted manifests");
|
||||
|
||||
JLOG(journal.warn()) << "Manifests: message had " << total
|
||||
<< " entries; processed all trusted plus the first "
|
||||
<< kMaxManifestsPerMessage << " untrusted";
|
||||
}
|
||||
|
||||
if (!relay.list().empty())
|
||||
{
|
||||
forEach([m2 = std::make_shared<Message>(relay, protocol::mtMANIFESTS)](
|
||||
@@ -1207,15 +1257,60 @@ OverlayImpl::getManifestsMessage()
|
||||
|
||||
if (auto seq = app_.getValidatorManifests().sequence(); seq != manifestListSeq_)
|
||||
{
|
||||
protocol::TMManifests tm;
|
||||
|
||||
// Phase 1: snapshot the cache under its own lock. Do not call
|
||||
// Validators::listed() here — that takes the validator-list lock, and
|
||||
// forEachManifest holds the manifest-cache lock, so consulting trust
|
||||
// inside the callback would invert the lock order used elsewhere
|
||||
// (see onManifests) and risk deadlock. Capture the manifest hash now,
|
||||
// while we have the Manifest object, for the suppression key.
|
||||
struct CachedManifest
|
||||
{
|
||||
PublicKey masterKey;
|
||||
std::string serialized;
|
||||
uint256 hash;
|
||||
};
|
||||
std::vector<CachedManifest> cached;
|
||||
app_.getValidatorManifests().forEachManifest(
|
||||
[&tm](std::size_t s) { tm.mutable_list()->Reserve(s); },
|
||||
[&tm, &hr = app_.getHashRouter()](Manifest const& manifest) {
|
||||
tm.add_list()->set_stobject(manifest.serialized.data(), manifest.serialized.size());
|
||||
hr.addSuppression(manifest.hash());
|
||||
[&cached](std::size_t s) { cached.reserve(s); },
|
||||
[&cached](Manifest const& manifest) {
|
||||
cached.push_back({manifest.masterKey, manifest.serialized, manifest.hash()});
|
||||
});
|
||||
|
||||
// Phase 2: no cache lock held, so trust checks are safe. Include every
|
||||
// trusted manifest, then fill any remaining headroom up to
|
||||
// kMaxManifestsPerMessage with untrusted gossip, so the whole message
|
||||
// stays within the per-message cap the receiver enforces (trusted
|
||||
// count is tiny in practice, so this effectively never drops trusted).
|
||||
std::vector<CachedManifest const*> selected;
|
||||
std::vector<CachedManifest const*> untrusted;
|
||||
for (auto const& e : cached)
|
||||
{
|
||||
if (app_.getValidators().listed(e.masterKey))
|
||||
{
|
||||
selected.push_back(&e);
|
||||
}
|
||||
else
|
||||
{
|
||||
untrusted.push_back(&e);
|
||||
}
|
||||
}
|
||||
|
||||
// Cap untrusted only; trusted manifests are all included above.
|
||||
auto const take = std::min(kMaxManifestsPerMessage, untrusted.size());
|
||||
selected.insert(selected.end(), untrusted.begin(), untrusted.begin() + take);
|
||||
|
||||
// Shuffle the order. Cryptographic randomness is not needed here.
|
||||
std::shuffle(selected.begin(), selected.end(), defaultPrng());
|
||||
|
||||
protocol::TMManifests tm;
|
||||
auto& hr = app_.getHashRouter();
|
||||
tm.mutable_list()->Reserve(static_cast<int>(selected.size()));
|
||||
for (auto const* e : selected)
|
||||
{
|
||||
tm.add_list()->set_stobject(e->serialized.data(), e->serialized.size());
|
||||
hr.addSuppression(e->hash);
|
||||
}
|
||||
|
||||
manifestMessage_.reset();
|
||||
|
||||
if (tm.list_size() != 0)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#include <xrpl/resource/Gossip.h>
|
||||
#include <xrpl/server/Handoff.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/server/Manifest.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
@@ -1131,6 +1132,9 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMManifests> const& m)
|
||||
if (s > 100)
|
||||
fee_.update(Resource::kFeeModerateBurdenPeer, "oversize");
|
||||
|
||||
// OverlayImpl::onManifests bounds the untrusted work and charges the fee
|
||||
// if the untrusted count exceeds the per-message cap; trusted manifests
|
||||
// are always processed and not counted against it.
|
||||
app_.getJobQueue().addJob(JtManifest, "RcvManifests", [this, that = shared_from_this(), m]() {
|
||||
overlay_.onManifests(m, that);
|
||||
});
|
||||
|
||||
@@ -351,6 +351,16 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin
|
||||
return result;
|
||||
}
|
||||
|
||||
// Drop an oversized TMManifests without penalty: consume the bytes and
|
||||
// return no error, so the connection is preserved.
|
||||
if (header->messageType == protocol::mtMANIFESTS &&
|
||||
(header->payloadWireSize > kMaximumManifestsMessageSize ||
|
||||
header->uncompressedSize > kMaximumManifestsMessageSize))
|
||||
{
|
||||
result.first = header->totalWireSize;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
|
||||
switch (header->messageType)
|
||||
|
||||
Reference in New Issue
Block a user