fix(export): defer validation-skewed relay shares

This commit is contained in:
Nicholas Dudfield
2026-07-14 18:27:56 +07:00
parent 18569dd931
commit 9286edc57f
9 changed files with 469 additions and 58 deletions

View File

@@ -3714,7 +3714,6 @@ class ConsensusExtensions_test : public beast::unit_test::suite
pendingLatch && pendingLatch->isFieldPresent(sfExportNode) &&
!pendingLatch->isFieldPresent(sfExportSignatureHash)))
return;
installValidated(originLedger);
auto release = ExportOriginMemo::releaseForm(
innerTx,
@@ -3782,6 +3781,44 @@ class ConsensusExtensions_test : public beast::unit_test::suite
expectNoShareEvent();
};
auto deferredCount = [&] {
std::lock_guard lock(ce.deferredExportSharesMutex_);
return ce.deferredExportShares_.size();
};
std::atomic<std::size_t> deferredCharges{0};
std::atomic<ExportShareCharge> lastDeferredCharge{
ExportShareCharge::none};
auto const deferredCharge = [&](ExportShareCharge const charge) {
lastDeferredCharge.store(charge, std::memory_order_relaxed);
deferredCharges.fetch_add(1, std::memory_order_relaxed);
};
auto admission = ce.onExportShare(share, deferredCharge);
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
BEAST_EXPECT(admission.charge == ExportShareCharge::none);
BEAST_EXPECT(deferredCount() == 1);
BEAST_EXPECT(!hasRetainedContribution());
admission = ce.onExportShare(share, deferredCharge);
BEAST_EXPECT(
admission.disposition == ExportShareDisposition::duplicate);
BEAST_EXPECT(deferredCount() == 1);
auto wrongBranch = share;
wrongBranch.originLedgerHash = makeHash("wrong-export-origin");
admission = ce.onExportShare(wrongBranch, deferredCharge);
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
BEAST_EXPECT(deferredCount() == 2);
auto beyondHorizon = share;
beyondHorizon.originLedgerSeq = universe->info().seq +
ConsensusExtensions::maxDeferredExportShareFutureLedgers_ + 1;
admission = ce.onExportShare(beyondHorizon, deferredCharge);
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
BEAST_EXPECT(deferredCount() == 2);
installValidated(originLedger);
if (replayWins)
{
std::unique_lock streamLock{ce.exportStreamMutex_};
@@ -3839,6 +3876,29 @@ class ConsensusExtensions_test : public beast::unit_test::suite
}
BEAST_EXPECT(hasRetainedContribution());
admission = ce.onExportShare(share, {});
BEAST_EXPECT(
admission.disposition == ExportShareDisposition::duplicate);
BEAST_EXPECT(admission.charge == ExportShareCharge::none);
admission = ce.onExportShare(wrongBranch, {});
BEAST_EXPECT(admission.disposition == ExportShareDisposition::invalid);
BEAST_EXPECT(admission.charge == ExportShareCharge::invalidData);
auto invalidSignature = share;
invalidSignature.signature = sign(
valKeys.keys->publicKey,
valKeys.keys->secretKey,
Slice{"invalid-export-share", 20});
admission = ce.onExportShare(invalidSignature, {});
BEAST_EXPECT(admission.disposition == ExportShareDisposition::invalid);
BEAST_EXPECT(admission.charge == ExportShareCharge::invalidSignature);
BEAST_EXPECT(deferredCount() == 0);
BEAST_EXPECT(deferredCharges.load(std::memory_order_relaxed) == 1);
BEAST_EXPECT(
lastDeferredCharge.load(std::memory_order_relaxed) ==
ExportShareCharge::invalidData);
BEAST_EXPECT(
ce.lastExportReplaySeq_.load(std::memory_order_relaxed) ==
originLedger->info().seq);
@@ -3882,7 +3942,15 @@ class ConsensusExtensions_test : public beast::unit_test::suite
BEAST_EXPECT(hasRetainedContribution());
expectNoShareEvent();
auto pendingAtStop = share;
pendingAtStop.originLedgerSeq = witnessed->info().seq + 1;
pendingAtStop.originLedgerHash = makeHash("pending-at-stop");
admission = ce.onExportShare(pendingAtStop, deferredCharge);
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
BEAST_EXPECT(deferredCount() == 1);
ce.stopExportShareService();
BEAST_EXPECT(deferredCount() == 0);
BEAST_EXPECT(
wsc->invoke("unsubscribe", stream)[jss::status] == "success");
}

View File

@@ -74,6 +74,19 @@ struct ResolvedExportShare
STTx releaseTarget;
};
enum class ExportShareResolutionStatus {
resolved,
deferred,
duplicate,
invalid
};
struct ExportShareResolution
{
ExportShareResolutionStatus status;
std::optional<ResolvedExportShare> value;
};
bool
isPendingExportShare(
ReadView const& view,
@@ -104,7 +117,7 @@ isPendingExportShare(
view.info().seq <= latch->getFieldU32(sfLastLedgerSequence);
}
std::optional<ResolvedExportShare>
ExportShareResolution
resolveExportShare(
Application& app,
ConsensusExtensions const& extensions,
@@ -112,26 +125,55 @@ resolveExportShare(
std::shared_ptr<Ledger const> const& validated,
beast::Journal j)
{
if (!validated || !validated->rules().enabled(featureExport) ||
share.triggerTxn != share.originTxn ||
!isPendingExportShare(*validated, share, j))
return std::nullopt;
if (!validated)
return {ExportShareResolutionStatus::deferred, std::nullopt};
if (!validated->rules().enabled(featureExport) ||
share.triggerTxn != share.originTxn)
return {ExportShareResolutionStatus::invalid, std::nullopt};
if (share.originLedgerSeq > validated->info().seq)
return {ExportShareResolutionStatus::deferred, std::nullopt};
auto const canonicalOriginHash =
share.originLedgerSeq == validated->info().seq
? std::optional<uint256>{validated->info().hash}
: hashOfSeq(*validated, share.originLedgerSeq, j);
if (!canonicalOriginHash)
return {ExportShareResolutionStatus::deferred, std::nullopt};
if (*canonicalOriginHash != share.originLedgerHash)
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const latchKey = keylet::shadowTicket(share.owner, share.originTxn);
auto const latch = validated->read(latchKey);
if (!latch)
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
if (latch->getType() != ltSHADOW_TICKET ||
!latch->isFieldPresent(sfTransactionHash) ||
!latch->isFieldPresent(sfExportUniverseHash) ||
!latch->isFieldPresent(sfExportCommittee) ||
!latch->isFieldPresent(sfLastLedgerSequence) ||
!latch->isFieldPresent(sfAccount) ||
!latch->isFieldPresent(sfLedgerSequence) ||
latch->getAccountID(sfAccount) != share.owner ||
latch->getFieldH256(sfTransactionHash) != share.originTxn ||
latch->getFieldU32(sfLedgerSequence) != share.originLedgerSeq)
return {ExportShareResolutionStatus::invalid, std::nullopt};
if (!latch->isFieldPresent(sfExportNode) ||
latch->isFieldPresent(sfExportSignatureHash) ||
validated->info().seq > latch->getFieldU32(sfLastLedgerSequence))
return {ExportShareResolutionStatus::duplicate, std::nullopt};
auto const originLedger =
app.getLedgerMaster().getLedgerByHash(share.originLedgerHash);
if (!originLedger || originLedger->info().seq != share.originLedgerSeq)
return std::nullopt;
if (!originLedger)
return {ExportShareResolutionStatus::deferred, std::nullopt};
if (originLedger->info().seq != share.originLedgerSeq)
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const [outer, _] = originLedger->txRead(share.originTxn);
if (!outer || outer->getTxnType() != ttEXPORT ||
!outer->isFieldPresent(sfExportedTxn) ||
outer->getAccountID(sfAccount) != share.owner)
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
bool const hasUniverse = outer->isFieldPresent(sfExportUniverseHash);
bool const hasCommittee = outer->isFieldPresent(sfExportCommittee);
@@ -142,15 +184,15 @@ resolveExportShare(
latch->getFieldH256(sfExportUniverseHash) ||
outer->getFieldVL(sfExportCommittee) !=
latch->getFieldVL(sfExportCommittee))))
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const universeHash = latch->getFieldH256(sfExportUniverseHash);
if (originLedger->info().parentHash != universeHash)
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const universeLedger =
app.getLedgerMaster().getLedgerByHash(universeHash);
if (!universeLedger)
return std::nullopt;
return {ExportShareResolutionStatus::deferred, std::nullopt};
auto const validatorView =
extensions.makeActiveValidatorView(universeLedger);
@@ -158,23 +200,23 @@ resolveExportShare(
*validatorView->sourceLedgerHash != universeHash ||
share.universePosition >=
validatorView->orderedOriginalMasterKeys.size())
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const committee = resolveExportCommittee(
makeSlice(latch->getFieldVL(sfExportCommittee)),
validatorView->orderedOriginalMasterKeys.size());
if (!committee || !committee->members.contains(share.universePosition))
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const& expectedMaster =
validatorView->orderedOriginalMasterKeys[share.universePosition];
if (app.validatorManifests().getMasterKey(share.signingKey) !=
expectedMaster)
return std::nullopt;
return {ExportShareResolutionStatus::duplicate, std::nullopt};
auto const inner = ExportLedgerOps::innerExportedTx(*outer);
if (!inner)
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
auto const targetNetworkID = inner->isFieldPresent(sfNetworkID)
? inner->getFieldU32(sfNetworkID)
: std::uint32_t{0};
@@ -189,9 +231,11 @@ resolveExportShare(
if (!identity || !release ||
ExportResultBuilder::exportIntentHash(identity.value()) !=
latch->getFieldH256(sfDigest))
return std::nullopt;
return {ExportShareResolutionStatus::invalid, std::nullopt};
return ResolvedExportShare{latch, std::move(release.value())};
return {
ExportShareResolutionStatus::resolved,
ResolvedExportShare{latch, std::move(release.value())}};
}
ExportShare
@@ -261,17 +305,144 @@ ConsensusExtensions::publishExportShareLocked(
bool
ConsensusExtensions::onExportShare(ExportShare const& share)
{
return admitExportShare(share, {}, true).isAccepted();
}
ExportShareAdmission
ConsensusExtensions::onExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge)
{
return admitExportShare(share, std::move(deferredCharge), true);
}
ExportShareAdmission
ConsensusExtensions::deferExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge,
LedgerIndex const validatedLedgerSeq)
{
if (share.originLedgerSeq <= validatedLedgerSeq ||
share.originLedgerSeq - validatedLedgerSeq >
maxDeferredExportShareFutureLedgers_)
return {ExportShareDisposition::deferred, ExportShareCharge::none};
std::size_t const serializedBytes = share.serialize().size();
auto const wireHash = share.wireHash();
std::lock_guard lock(deferredExportSharesMutex_);
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
return false;
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
if (auto const it = deferredExportShares_.find(wireHash);
it != deferredExportShares_.end())
{
if (!it->second.charge && deferredCharge)
it->second.charge = std::move(deferredCharge);
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
}
bool const newOrigin =
!deferredExportShareOrigins_.contains(share.originTxn);
if (deferredExportShares_.size() >= maxDeferredExportShares_ ||
serializedBytes > maxDeferredExportShareBytes_ ||
deferredExportShareBytes_ >
maxDeferredExportShareBytes_ - serializedBytes ||
(newOrigin &&
deferredExportShareOrigins_.size() >= maxDeferredExportShareOrigins_))
return {ExportShareDisposition::deferred, ExportShareCharge::none};
deferredExportShareBytes_ += serializedBytes;
++deferredExportShareOrigins_[share.originTxn];
deferredExportShares_.emplace(
wireHash,
DeferredExportShare{share, std::move(deferredCharge), serializedBytes});
return {ExportShareDisposition::deferred, ExportShareCharge::none};
}
void
ConsensusExtensions::retryDeferredExportShares(
LedgerIndex const validatedLedgerSeq)
{
std::vector<DeferredExportShare> retry;
{
std::lock_guard lock(deferredExportSharesMutex_);
for (auto it = deferredExportShares_.begin();
it != deferredExportShares_.end();)
{
if (it->second.share.originLedgerSeq > validatedLedgerSeq)
{
++it;
continue;
}
auto const origin = it->second.share.originTxn;
deferredExportShareBytes_ -= it->second.serializedBytes;
auto const originIt = deferredExportShareOrigins_.find(origin);
if (originIt != deferredExportShareOrigins_.end() &&
--originIt->second == 0)
deferredExportShareOrigins_.erase(originIt);
retry.push_back(std::move(it->second));
it = deferredExportShares_.erase(it);
}
}
for (auto& deferred : retry)
{
auto const result = admitExportShare(deferred.share, {}, false);
if (result.isAccepted())
{
auto const frame = deferred.share.serialize();
protocol::TMExportShares message;
message.add_shares(frame.data(), frame.size());
app_.overlay().broadcast(message);
}
else if (
result.disposition == ExportShareDisposition::invalid &&
result.charge != ExportShareCharge::none && deferred.charge)
{
deferred.charge(result.charge);
}
}
}
void
ConsensusExtensions::clearDeferredExportShares()
{
std::lock_guard lock(deferredExportSharesMutex_);
deferredExportShares_.clear();
deferredExportShareOrigins_.clear();
deferredExportShareBytes_ = 0;
}
ExportShareAdmission
ConsensusExtensions::admitExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge,
bool const allowDeferral)
{
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
auto const validated = app_.getLedgerMaster().getValidatedLedger();
auto const resolved = resolveExportShare(app_, *this, share, validated, j_);
if (!resolved)
return false;
if (allowDeferral && validated &&
share.originLedgerSeq > validated->info().seq)
return deferExportShare(
share, std::move(deferredCharge), validated->info().seq);
auto resolved = resolveExportShare(app_, *this, share, validated, j_);
if (resolved.status == ExportShareResolutionStatus::deferred)
return {ExportShareDisposition::deferred, ExportShareCharge::none};
if (resolved.status == ExportShareResolutionStatus::duplicate)
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
if (resolved.status == ExportShareResolutionStatus::invalid ||
!resolved.value || !validated)
return {
ExportShareDisposition::invalid, ExportShareCharge::invalidData};
if (!postValidationExportSigCollector_.reopenPublication(
share.originTxn, share.triggerTxn, validated->info().seq))
return false;
return {ExportShareDisposition::deferred, ExportShareCharge::none};
ExportSigCollectorV2::Contribution contribution{
share.universePosition, share.signingKey, share.signature};
@@ -279,10 +450,31 @@ ConsensusExtensions::onExportShare(ExportShare const& share)
share.originTxn, std::move(contribution), validated->info().seq);
if (admission.result != ExportSigCollectorV2::BeginResult::verify ||
!admission.ticket)
return false;
{
switch (admission.result)
{
case ExportSigCollectorV2::BeginResult::duplicate:
case ExportSigCollectorV2::BeginResult::conflicted:
return {
ExportShareDisposition::duplicate, ExportShareCharge::none};
case ExportSigCollectorV2::BeginResult::unknownOrigin:
case ExportSigCollectorV2::BeginResult::capacity:
return {
ExportShareDisposition::deferred, ExportShareCharge::none};
case ExportSigCollectorV2::BeginResult::malformed:
return {
ExportShareDisposition::invalid,
ExportShareCharge::invalidData};
case ExportSigCollectorV2::BeginResult::verify:
break;
}
return {
ExportShareDisposition::invalid, ExportShareCharge::invalidData};
}
auto const signer = calcAccountID(share.signingKey);
auto const data = buildMultiSigningData(resolved->releaseTarget, signer);
auto const data =
buildMultiSigningData(resolved.value->releaseTarget, signer);
auto const signatureVerified = verify(
share.signingKey,
data.slice(),
@@ -293,17 +485,21 @@ ConsensusExtensions::onExportShare(ExportShare const& share)
{
std::lock_guard streamLock(exportStreamMutex_);
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
return false;
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
auto const latest = app_.getLedgerMaster().getValidatedLedger();
if (!latest || !isPendingExportShare(*latest, share, j_))
return false;
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
publishExportShareLocked(
share, latest->info().seq, latest->info().hash);
return true;
return {ExportShareDisposition::accepted, ExportShareCharge::none};
}
if (outcome.result == ExportSigCollectorV2::AdmitResult::invalid)
return {
ExportShareDisposition::invalid,
ExportShareCharge::invalidSignature};
if (outcome.result != ExportSigCollectorV2::AdmitResult::conflicted ||
!outcome.priorContribution || !outcome.conflictingContribution)
return false;
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
// Propagate both valid encodings so every honest collector can observe the
// same absorbing conflict even if it missed the first frame.
@@ -315,7 +511,7 @@ ConsensusExtensions::onExportShare(ExportShare const& share)
conflict.add_shares(frame.data(), frame.size());
}
app_.overlay().broadcast(conflict);
return false;
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
}
void
@@ -395,6 +591,11 @@ ConsensusExtensions::onValidatedLedger(
}
}
// Retained replay owns the start of this cursor. Deferred relay shares
// are reconsidered afterward, so newly accepted entries publish as
// edge events before this node authors its own shares below.
retryDeferredExportShares(validated->info().seq);
std::vector<ExportShare> shares;
shares.reserve(ExportLimits::maxLiveExportLatches);
auto const& keys = app_.getValidatorKeys();
@@ -551,6 +752,7 @@ ConsensusExtensions::onValidatedLedger(
void
ConsensusExtensions::startExportShareService()
{
clearDeferredExportShares();
std::lock_guard streamLock(exportStreamMutex_);
exportStreamEmissionSeq_ = 0;
exportStreamEmittedShares_.clear();
@@ -562,9 +764,12 @@ void
ConsensusExtensions::stopExportShareService() noexcept
{
exportShareServiceStarted_.store(false, std::memory_order_release);
std::lock_guard streamLock(exportStreamMutex_);
exportStreamEmissionSeq_ = 0;
exportStreamEmittedShares_.clear();
{
std::lock_guard streamLock(exportStreamMutex_);
exportStreamEmissionSeq_ = 0;
exportStreamEmittedShares_.clear();
}
clearDeferredExportShares();
}
//------------------------------------------------------------------------------
@@ -3180,7 +3385,8 @@ ConsensusExtensions::attachExportSignatures(
contribution.position,
contribution.signingKey,
contribution.signature};
if (!resolveExportShare(app_, *this, share, validated, j_))
if (resolveExportShare(app_, *this, share, validated, j_).status !=
ExportShareResolutionStatus::resolved)
continue;
auto const frame = share.serialize();

View File

@@ -8,6 +8,7 @@
#include <xrpld/app/misc/ExportSigCollectorV2.h>
#include <xrpld/consensus/ConsensusParms.h>
#include <xrpld/consensus/ConsensusTypes.h>
#include <xrpld/overlay/ExportShareAdmission.h>
#include <xrpld/overlay/Message.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpl/basics/Buffer.h>
@@ -61,12 +62,50 @@ class ConsensusExtensions
std::atomic<bool> exportShareServiceStarted_{false};
std::atomic<LedgerIndex> lastExportReplaySeq_{0};
struct DeferredExportShare
{
ExportShare share;
ExportShareChargeHandler charge;
std::size_t serializedBytes;
};
static constexpr std::size_t maxDeferredExportShareOrigins_ =
ExportLimits::maxLiveExportLatches;
static constexpr std::size_t maxDeferredExportShares_ =
ExportLimits::maxLiveExportLatches * ExportLimits::maxCommitteeMembers;
static constexpr std::size_t maxDeferredExportShareBytes_ =
maxDeferredExportShares_ * ExportLimits::maxSerializedExportShareBytes;
static constexpr LedgerIndex maxDeferredExportShareFutureLedgers_ = 8;
std::mutex deferredExportSharesMutex_;
std::map<uint256, DeferredExportShare> deferredExportShares_;
std::map<uint256, std::size_t> deferredExportShareOrigins_;
std::size_t deferredExportShareBytes_{0};
bool
publishExportShareLocked(
ExportShare const& share,
LedgerIndex validatedLedgerSeq,
uint256 const& validatedLedgerHash);
ExportShareAdmission
admitExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge,
bool allowDeferral);
ExportShareAdmission
deferExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge,
LedgerIndex validatedLedgerSeq);
void
retryDeferredExportShares(LedgerIndex validatedLedgerSeq);
void
clearDeferredExportShares();
public:
beast::Journal j_; // public: accessed by extensionsTick template
@@ -203,6 +242,11 @@ public:
bool
onExportShare(ExportShare const& share);
ExportShareAdmission
onExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge);
/** Release local shares unlocked by an exact network-validated ledger. */
void
onValidatedLedger(LedgerIndex seq, uint256 const& hash) noexcept;

View File

@@ -1612,10 +1612,18 @@ ApplicationImp::start(bool withTimers)
{
auto const weak =
std::weak_ptr<ConsensusExtensions>{consensusExtensions_};
overlay_->setExportShareHandler([weak](ExportShare const& share) {
auto const extensions = weak.lock();
return extensions && extensions->onExportShare(share);
});
overlay_->setExportShareHandler(
[weak](
ExportShare const& share,
ExportShareChargeHandler deferredCharge) {
auto const extensions = weak.lock();
if (!extensions)
return ExportShareAdmission{
ExportShareDisposition::deferred,
ExportShareCharge::none};
return extensions->onExportShare(
share, std::move(deferredCharge));
});
consensusExtensions_->startExportShareService();
overlay_->start();
}

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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 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_OVERLAY_EXPORTSHAREADMISSION_H_INCLUDED
#define RIPPLE_OVERLAY_EXPORTSHAREADMISSION_H_INCLUDED
#include <cstdint>
#include <functional>
namespace ripple {
enum class ExportShareDisposition : std::uint8_t {
accepted,
deferred,
duplicate,
invalid
};
enum class ExportShareCharge : std::uint8_t {
none,
invalidData,
invalidSignature
};
struct ExportShareAdmission
{
ExportShareDisposition disposition{ExportShareDisposition::invalid};
ExportShareCharge charge{ExportShareCharge::none};
bool
isAccepted() const
{
return disposition == ExportShareDisposition::accepted;
}
};
using ExportShareChargeHandler = std::function<void(ExportShareCharge)>;
} // namespace ripple
#endif

View File

@@ -20,6 +20,7 @@
#ifndef RIPPLE_OVERLAY_OVERLAY_H_INCLUDED
#define RIPPLE_OVERLAY_OVERLAY_H_INCLUDED
#include <xrpld/overlay/ExportShareAdmission.h>
#include <xrpld/overlay/Peer.h>
#include <xrpld/overlay/PeerSet.h>
#include <xrpl/beast/utility/PropertyStream.h>
@@ -65,12 +66,9 @@ protected:
public:
enum class Promote { automatic, never, always };
/** Application admission for a structurally valid ExportShare.
Returning true means the application has verified the share against
its origin context, committee position/key, and target multisignature.
*/
using ExportShareHandler = std::function<bool(ExportShare const&)>;
/** Application admission for a structurally valid ExportShare. */
using ExportShareHandler = std::function<
ExportShareAdmission(ExportShare const&, ExportShareChargeHandler)>;
struct Setup
{
@@ -176,8 +174,10 @@ public:
With no callback installed, admission fails closed.
*/
virtual bool
acceptExportShare(ExportShare const& share) = 0;
virtual ExportShareAdmission
acceptExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge) = 0;
/** Relay a proposal.
* @param m the serialized proposal

View File

@@ -1226,8 +1226,10 @@ OverlayImpl::setExportShareHandler(ExportShareHandler handler)
exportShareHandler_ = std::move(handler);
}
bool
OverlayImpl::acceptExportShare(ExportShare const& share)
ExportShareAdmission
OverlayImpl::acceptExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge)
{
ExportShareHandler handler;
{
@@ -1236,11 +1238,11 @@ OverlayImpl::acceptExportShare(ExportShare const& share)
}
if (!handler)
return false;
return {ExportShareDisposition::deferred, ExportShareCharge::none};
try
{
return handler(share);
return handler(share, std::move(deferredCharge));
}
catch (std::exception const& e)
{
@@ -1251,7 +1253,7 @@ OverlayImpl::acceptExportShare(ExportShare const& share)
{
JLOG(journal_.error()) << "ExportShare admission callback failed";
}
return false;
return {ExportShareDisposition::deferred, ExportShareCharge::none};
}
std::set<Peer::id_t>

View File

@@ -235,8 +235,10 @@ public:
void
setExportShareHandler(ExportShareHandler handler) override;
bool
acceptExportShare(ExportShare const& share) override;
ExportShareAdmission
acceptExportShare(
ExportShare const& share,
ExportShareChargeHandler deferredCharge) override;
std::set<Peer::id_t>
relay(

View File

@@ -64,6 +64,21 @@ std::chrono::milliseconds constexpr peerHighLatency{300};
/** How often we PING the peer to check for latency and sendq probe */
std::chrono::seconds constexpr peerTimerInterval{60};
Resource::Charge const*
exportShareFee(ExportShareCharge const charge)
{
switch (charge)
{
case ExportShareCharge::none:
return nullptr;
case ExportShareCharge::invalidData:
return &Resource::feeInvalidData;
case ExportShareCharge::invalidSignature:
return &Resource::feeInvalidSignature;
}
return nullptr;
}
} // namespace
// TODO: Remove this exclusion once unit tests are added after the hotfix
@@ -1151,10 +1166,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMExportShares> const& m)
}
if (fresh.empty())
{
fee_.update(Resource::feeUselessData, "duplicate export shares");
return;
}
std::weak_ptr<PeerImp> weak = shared_from_this();
app_.getJobQueue().addJob(
@@ -1169,7 +1181,16 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMExportShares> const& m)
accepted.mutable_shares()->Reserve(fresh.size());
for (auto const index : fresh)
{
if (peer->overlay_.acceptExportShare(shares[index]))
auto const chargeDeferred =
[weak](ExportShareCharge const charge) {
auto const peer = weak.lock();
auto const fee = exportShareFee(charge);
if (peer && fee)
peer->charge(*fee, "deferred export share");
};
auto const admission = peer->overlay_.acceptExportShare(
shares[index], chargeDeferred);
if (admission.isAccepted())
{
// Stable raw-wire routing begins only after semantic
// admission; an early state-relative rejection must not
@@ -1178,6 +1199,10 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMExportShares> const& m)
shares[index].wireHash(), peer->id_);
accepted.add_shares(m->shares(index));
}
else if (auto const fee = exportShareFee(admission.charge))
{
peer->charge(*fee, "export share");
}
}
// Structural validity is insufficient: only application-admitted