mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-31 02:50:10 +00:00
feat: implement ephemeral export signature collection
Replace on-ledger ttEXPORT_SIGN transactions with ephemeral signature collection via TMValidation messages. This eliminates O(n²) metadata bloat from accumulating signatures on-ledger. Changes: - Add ExportSignatureCollector for in-memory signature storage with quorum tracking (80% UNL threshold) - Extend TMValidation protobuf with exportSignatures field - Sign pending exports during validate() and broadcast via validation - Extract signatures from received TMValidation in PeerImp - TxQ checks quorum from memory instead of ledger - Inject ttEXPORT when quorum reached (can be ledger N+1 or N+2) - Clean up collector after ttEXPORT processed Includes [EXPORT-TIMING] debug logging for timing analysis.
This commit is contained in:
@@ -407,6 +407,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/misc/CanonicalTXSet.cpp
|
||||
src/ripple/app/misc/FeeVoteImpl.cpp
|
||||
src/ripple/app/misc/HashRouter.cpp
|
||||
src/ripple/app/misc/impl/ExportSignatureCollector.cpp
|
||||
src/ripple/app/misc/NegativeUNLVote.cpp
|
||||
src/ripple/app/misc/NetworkOPs.cpp
|
||||
src/ripple/app/misc/SHAMapStoreImp.cpp
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <ripple/app/ledger/LocalTxs.h>
|
||||
#include <ripple/app/ledger/OpenLedger.h>
|
||||
#include <ripple/app/misc/AmendmentTable.h>
|
||||
#include <ripple/app/misc/ExportSignatureCollector.h>
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/app/misc/LoadFeeTrack.h>
|
||||
#include <ripple/app/misc/NegativeUNLVote.h>
|
||||
@@ -669,6 +670,10 @@ RCLConsensus::Adaptor::doAccept(
|
||||
rules = makeRulesGivenLedger(*lastVal, app_.config().features);
|
||||
else
|
||||
rules.emplace(app_.config().features);
|
||||
JLOG(j_.info())
|
||||
<< "[EXPORT-TIMING] onAccept: openLedger().accept() START for seq="
|
||||
<< built.ledger_->info().seq + 1
|
||||
<< " (parent=" << built.ledger_->info().seq << ")";
|
||||
app_.openLedger().accept(
|
||||
app_,
|
||||
*rules,
|
||||
@@ -679,53 +684,16 @@ RCLConsensus::Adaptor::doAccept(
|
||||
tapNONE,
|
||||
"consensus",
|
||||
[&](OpenView& view, beast::Journal j) {
|
||||
DBG_EXPORT("consensus callback seq=" << view.info().seq);
|
||||
//@@start export-sign-submit
|
||||
// Generate ttEXPORT_SIGN UVTxns if we're a validator on the
|
||||
// UNLReport. In standalone mode we queue via rawTxInsert so
|
||||
// it's applied when this open ledger closes. In network mode
|
||||
// we submit for relay to other validators.
|
||||
if (view.rules().enabled(featureExport))
|
||||
{
|
||||
auto exportSignTxns = makeExportSignTxns(view, app_, j_);
|
||||
for (auto const& tx : exportSignTxns)
|
||||
{
|
||||
uint256 txID = tx->getTransactionID();
|
||||
app_.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
|
||||
if (app_.config().standalone())
|
||||
{
|
||||
// Standalone: queue in open ledger, applied when it
|
||||
// closes
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
tx->add(*s);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
DBG_EXPORT(
|
||||
"[EXPORT-TRACE] STEP-2a: rawTxInsert "
|
||||
"ttEXPORT_SIGN txID="
|
||||
<< txID << " callbackSeq=" << view.info().seq);
|
||||
DBG_EXPORT(
|
||||
"ttEXPORT_SIGN JSON:\n"
|
||||
<< tx->getJson(JsonOptions::none)
|
||||
.toStyledString());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Network: submit for relay to other validators
|
||||
app_.getOPs().submitTransaction(tx);
|
||||
DBG_EXPORT(
|
||||
"network: submitted ttEXPORT_SIGN txID="
|
||||
<< txID);
|
||||
DBG_EXPORT(
|
||||
"ttEXPORT_SIGN JSON:\n"
|
||||
<< tx->getJson(JsonOptions::none)
|
||||
.toStyledString());
|
||||
}
|
||||
}
|
||||
}
|
||||
//@@end export-sign-submit
|
||||
JLOG(j.info()) << "[EXPORT-TIMING] TxQ.accept callback seq="
|
||||
<< view.info().seq;
|
||||
// Export signatures are now collected ephemerally via
|
||||
// validation messages (signPendingExports in validate()),
|
||||
// not via ttEXPORT_SIGN transactions. This eliminates the
|
||||
// O(n²) metadata bloat from accumulating signatures on-ledger.
|
||||
return app_.getTxQ().accept(app_, view);
|
||||
});
|
||||
JLOG(j_.info())
|
||||
<< "[EXPORT-TIMING] onAccept: openLedger().accept() END";
|
||||
|
||||
// Signal a potential fee change to subscribers after the open ledger
|
||||
// is created
|
||||
@@ -940,9 +908,42 @@ RCLConsensus::Adaptor::validate(
|
||||
|
||||
handleNewValidation(app_, v, "local");
|
||||
|
||||
JLOG(j_.info()) << "[EXPORT-TIMING] validate(): signing exports for seq="
|
||||
<< ledger.seq();
|
||||
|
||||
// Sign pending exports and collect signatures
|
||||
auto exportSigs = signPendingExports(*ledger.ledger_, app_, j_);
|
||||
|
||||
JLOG(j_.info()) << "[EXPORT-TIMING] validate(): signed "
|
||||
<< exportSigs.size() << " exports for seq=" << ledger.seq();
|
||||
|
||||
// Store our own signatures in memory
|
||||
auto const currentSeq = ledger.ledger_->info().seq;
|
||||
for (auto const& [txnHash, signer] : exportSigs)
|
||||
{
|
||||
JLOG(j_.info())
|
||||
<< "[EXPORT-TIMING] validate(): storing OWN signature for txn="
|
||||
<< txnHash << " seq=" << currentSeq;
|
||||
app_.getExportSignatureCollector().addSignature(
|
||||
txnHash, app_.getValidationPublicKey(), signer, currentSeq);
|
||||
}
|
||||
|
||||
// Broadcast to all our peers:
|
||||
protocol::TMValidation val;
|
||||
val.set_validation(serialized.data(), serialized.size());
|
||||
|
||||
// Add export signatures to the validation message
|
||||
for (auto const& [txnHash, signer] : exportSigs)
|
||||
{
|
||||
Serializer s;
|
||||
s.addBitString(txnHash);
|
||||
signer.add(s);
|
||||
val.add_exportsignatures(s.data(), s.size());
|
||||
}
|
||||
|
||||
JLOG(j_.info())
|
||||
<< "[EXPORT-TIMING] validate(): broadcasting TMValidation with "
|
||||
<< exportSigs.size() << " export sigs for seq=" << ledger.seq();
|
||||
app_.overlay().broadcast(val);
|
||||
|
||||
// Publish to all our subscribers:
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <ripple/app/main/Tuning.h>
|
||||
#include <ripple/app/misc/AmendmentTable.h>
|
||||
#include <ripple/app/misc/DatagramMonitor.h>
|
||||
#include <ripple/app/misc/ExportSignatureCollector.h>
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/app/misc/LoadFeeTrack.h>
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
@@ -218,6 +219,7 @@ public:
|
||||
std::unique_ptr<AmendmentTable> m_amendmentTable;
|
||||
std::unique_ptr<LoadFeeTrack> mFeeTrack;
|
||||
std::unique_ptr<HashRouter> hashRouter_;
|
||||
std::unique_ptr<ExportSignatureCollector> exportSignatureCollector_;
|
||||
RCLValidations mValidations;
|
||||
std::unique_ptr<LoadManager> m_loadManager;
|
||||
std::unique_ptr<TxQ> txQ_;
|
||||
@@ -462,6 +464,9 @@ public:
|
||||
stopwatch(),
|
||||
HashRouter::getDefaultHoldTime()))
|
||||
|
||||
, exportSignatureCollector_(std::make_unique<ExportSignatureCollector>(
|
||||
logs_->journal("ExportSignatureCollector")))
|
||||
|
||||
, mValidations(
|
||||
ValidationParms(),
|
||||
stopwatch(),
|
||||
@@ -818,6 +823,12 @@ public:
|
||||
return *hashRouter_;
|
||||
}
|
||||
|
||||
ExportSignatureCollector&
|
||||
getExportSignatureCollector() override
|
||||
{
|
||||
return *exportSignatureCollector_;
|
||||
}
|
||||
|
||||
RCLValidations&
|
||||
getValidations() override
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ using SLE = STLedgerEntry;
|
||||
using CachedSLEs = TaggedCache<uint256, SLE const>;
|
||||
|
||||
class CollectorManager;
|
||||
class ExportSignatureCollector;
|
||||
class Family;
|
||||
class HashRouter;
|
||||
class Logs;
|
||||
@@ -184,6 +185,8 @@ public:
|
||||
getAmendmentTable() = 0;
|
||||
virtual HashRouter&
|
||||
getHashRouter() = 0;
|
||||
virtual ExportSignatureCollector&
|
||||
getExportSignatureCollector() = 0;
|
||||
virtual LoadFeeTrack&
|
||||
getFeeTrack() = 0;
|
||||
virtual LoadManager&
|
||||
|
||||
156
src/ripple/app/misc/ExportSignatureCollector.h
Normal file
156
src/ripple/app/misc/ExportSignatureCollector.h
Normal file
@@ -0,0 +1,156 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 Ripple Labs Inc.
|
||||
|
||||
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_MISC_EXPORTSIGNATURECOLLECTOR_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_EXPORTSIGNATURECOLLECTOR_H_INCLUDED
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/beast/utility/Journal.h>
|
||||
#include <ripple/protocol/Protocol.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <ripple/protocol/STArray.h>
|
||||
#include <ripple/protocol/STObject.h>
|
||||
#include <ripple/protocol/UintTypes.h>
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Application;
|
||||
class ReadView;
|
||||
|
||||
/** Collects validator signatures for pending exports.
|
||||
|
||||
Export signatures are collected via validation messages rather than
|
||||
on-ledger transactions. This eliminates the O(n²) metadata bloat that
|
||||
occurs when accumulating signatures in ledger entries.
|
||||
|
||||
The collector stores signatures in memory until quorum is reached,
|
||||
at which point a ttEXPORT transaction can be created with all signatures.
|
||||
|
||||
Thread safety: All public methods are thread-safe.
|
||||
*/
|
||||
class ExportSignatureCollector
|
||||
{
|
||||
public:
|
||||
ExportSignatureCollector(beast::Journal journal);
|
||||
|
||||
/** Add a signature for an export.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param validator The public key of the signing validator
|
||||
@param signer The STObject containing the signature (sfSigner)
|
||||
@param currentSeq Current ledger sequence (for tracking first-seen)
|
||||
*/
|
||||
void
|
||||
addSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
STObject signer,
|
||||
LedgerIndex currentSeq);
|
||||
|
||||
/** Get all signatures collected for an export.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@return STArray of signer objects, empty if none collected
|
||||
*/
|
||||
STArray
|
||||
getSignatures(uint256 const& txnHash) const;
|
||||
|
||||
/** Get the number of signatures collected for an export.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@return Number of unique validator signatures
|
||||
*/
|
||||
std::size_t
|
||||
signatureCount(uint256 const& txnHash) const;
|
||||
|
||||
/** Check if an export has reached quorum.
|
||||
|
||||
Quorum is 80% of the UNL (rounded up).
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param view The current ledger view (for UNL size)
|
||||
@param app The application (for UNL access)
|
||||
@return true if quorum reached
|
||||
*/
|
||||
bool
|
||||
hasQuorum(uint256 const& txnHash, ReadView const& view, Application& app)
|
||||
const;
|
||||
|
||||
/** Get all pending exports that have reached quorum.
|
||||
|
||||
@param view The current ledger view
|
||||
@param app The application
|
||||
@return Vector of txnHashes that have quorum
|
||||
*/
|
||||
std::vector<uint256>
|
||||
getExportsWithQuorum(ReadView const& view, Application& app) const;
|
||||
|
||||
/** Get all pending export hashes.
|
||||
|
||||
@return Vector of all txnHashes being tracked
|
||||
*/
|
||||
std::vector<uint256>
|
||||
getPendingExports() const;
|
||||
|
||||
/** Clear signatures for a completed export.
|
||||
|
||||
Called after ttEXPORT is applied to clean up memory.
|
||||
|
||||
@param txnHash The hash of the completed export
|
||||
*/
|
||||
void
|
||||
clearForTxn(uint256 const& txnHash);
|
||||
|
||||
/** Clean up stale exports that haven't reached quorum.
|
||||
|
||||
Prevents memory leak from orphaned exports.
|
||||
|
||||
@param currentSeq Current ledger sequence
|
||||
@param maxAge Maximum age in ledgers before cleanup (default 256)
|
||||
*/
|
||||
void
|
||||
cleanupStale(LedgerIndex currentSeq, LedgerIndex maxAge = 256);
|
||||
|
||||
private:
|
||||
// Map<txnHash, Map<validatorPubKey, SignerObject>>
|
||||
std::map<uint256, std::map<PublicKey, STObject>> signatures_;
|
||||
|
||||
// Track when each export was first seen (for timeout)
|
||||
std::map<uint256, LedgerIndex> firstSeenLedger_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
beast::Journal j_;
|
||||
|
||||
/** Get UNL size from the view.
|
||||
|
||||
@param view The current ledger view
|
||||
@param app The application
|
||||
@return Number of validators in UNL, or 1 if not available
|
||||
*/
|
||||
std::size_t
|
||||
getUNLSize(ReadView const& view, Application& app) const;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
226
src/ripple/app/misc/impl/ExportSignatureCollector.cpp
Normal file
226
src/ripple/app/misc/impl/ExportSignatureCollector.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 Ripple Labs Inc.
|
||||
|
||||
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 <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/ExportSignatureCollector.h>
|
||||
#include <ripple/app/misc/ValidatorList.h>
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/SField.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
ExportSignatureCollector::ExportSignatureCollector(beast::Journal journal)
|
||||
: j_(journal)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
ExportSignatureCollector::addSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
STObject signer,
|
||||
LedgerIndex currentSeq)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
// Track first-seen time for cleanup
|
||||
if (firstSeenLedger_.find(txnHash) == firstSeenLedger_.end())
|
||||
{
|
||||
firstSeenLedger_[txnHash] = currentSeq;
|
||||
JLOG(j_.debug()) << "ExportSignatureCollector: first signature for "
|
||||
<< txnHash << " at ledger " << currentSeq;
|
||||
}
|
||||
|
||||
// Add or update signature for this validator
|
||||
auto& signerMap = signatures_[txnHash];
|
||||
auto [it, inserted] = signerMap.emplace(validator, std::move(signer));
|
||||
|
||||
if (inserted)
|
||||
{
|
||||
JLOG(j_.debug()) << "ExportSignatureCollector: added signature from "
|
||||
<< toBase58(TokenType::NodePublic, validator)
|
||||
<< " for " << txnHash
|
||||
<< " (total: " << signerMap.size() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
STArray
|
||||
ExportSignatureCollector::getSignatures(uint256 const& txnHash) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
STArray signers(sfSigners);
|
||||
|
||||
auto it = signatures_.find(txnHash);
|
||||
if (it != signatures_.end())
|
||||
{
|
||||
for (auto const& [pk, signer] : it->second)
|
||||
{
|
||||
signers.push_back(signer);
|
||||
}
|
||||
}
|
||||
|
||||
return signers;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ExportSignatureCollector::signatureCount(uint256 const& txnHash) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto it = signatures_.find(txnHash);
|
||||
if (it != signatures_.end())
|
||||
return it->second.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ExportSignatureCollector::getUNLSize(ReadView const& view, Application& app)
|
||||
const
|
||||
{
|
||||
// For first 256 ledgers, UNLReport may not exist
|
||||
// In standalone mode, we're the only validator
|
||||
auto const seq = view.info().seq;
|
||||
if (seq < 256 || app.config().standalone())
|
||||
return 1;
|
||||
|
||||
// Try to get UNL size from UNLReport
|
||||
auto const unlReportKey = keylet::UNLReport();
|
||||
auto const sle = view.read(unlReportKey);
|
||||
if (sle && sle->isFieldPresent(sfActiveValidators))
|
||||
{
|
||||
return sle->getFieldArray(sfActiveValidators).size();
|
||||
}
|
||||
|
||||
// Fallback: use validator list count
|
||||
auto const count = app.validators().count();
|
||||
return count > 0 ? count : 1;
|
||||
}
|
||||
|
||||
bool
|
||||
ExportSignatureCollector::hasQuorum(
|
||||
uint256 const& txnHash,
|
||||
ReadView const& view,
|
||||
Application& app) const
|
||||
{
|
||||
auto const sigCount = signatureCount(txnHash);
|
||||
auto const unlSize = getUNLSize(view, app);
|
||||
|
||||
// Quorum is 80% of UNL, rounded up
|
||||
auto const threshold = (unlSize * 80 + 99) / 100;
|
||||
|
||||
JLOG(j_.trace()) << "ExportSignatureCollector::hasQuorum: " << txnHash
|
||||
<< " sigCount=" << sigCount << " unlSize=" << unlSize
|
||||
<< " threshold=" << threshold;
|
||||
|
||||
return sigCount >= threshold;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
ExportSignatureCollector::getExportsWithQuorum(
|
||||
ReadView const& view,
|
||||
Application& app) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
std::vector<uint256> ready;
|
||||
auto const unlSize = getUNLSize(view, app);
|
||||
auto const threshold = (unlSize * 80 + 99) / 100;
|
||||
|
||||
for (auto const& [txnHash, signerMap] : signatures_)
|
||||
{
|
||||
if (signerMap.size() >= threshold)
|
||||
{
|
||||
ready.push_back(txnHash);
|
||||
JLOG(j_.debug())
|
||||
<< "ExportSignatureCollector: quorum reached for " << txnHash
|
||||
<< " (" << signerMap.size() << "/" << unlSize << ")";
|
||||
}
|
||||
}
|
||||
|
||||
return ready;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
ExportSignatureCollector::getPendingExports() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
std::vector<uint256> pending;
|
||||
pending.reserve(signatures_.size());
|
||||
|
||||
for (auto const& [txnHash, _] : signatures_)
|
||||
{
|
||||
pending.push_back(txnHash);
|
||||
}
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
void
|
||||
ExportSignatureCollector::clearForTxn(uint256 const& txnHash)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto sigCount = signatures_.erase(txnHash);
|
||||
auto seqCount = firstSeenLedger_.erase(txnHash);
|
||||
|
||||
if (sigCount > 0 || seqCount > 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "ExportSignatureCollector: cleared " << txnHash;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ExportSignatureCollector::cleanupStale(
|
||||
LedgerIndex currentSeq,
|
||||
LedgerIndex maxAge)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
std::vector<uint256> toRemove;
|
||||
|
||||
for (auto const& [txnHash, firstSeen] : firstSeenLedger_)
|
||||
{
|
||||
if (currentSeq > firstSeen + maxAge)
|
||||
{
|
||||
toRemove.push_back(txnHash);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& txnHash : toRemove)
|
||||
{
|
||||
JLOG(j_.warn()) << "ExportSignatureCollector: cleaning up stale export "
|
||||
<< txnHash
|
||||
<< " (age: " << (currentSeq - firstSeenLedger_[txnHash])
|
||||
<< " ledgers)";
|
||||
|
||||
signatures_.erase(txnHash);
|
||||
firstSeenLedger_.erase(txnHash);
|
||||
}
|
||||
|
||||
if (!toRemove.empty())
|
||||
{
|
||||
JLOG(j_.info()) << "ExportSignatureCollector: cleaned up "
|
||||
<< toRemove.size() << " stale exports";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <ripple/app/ledger/OpenLedger.h>
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/ExportSignatureCollector.h>
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/app/misc/LoadFeeTrack.h>
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
@@ -1669,24 +1670,33 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exportedLgrSeq < seq - 1)
|
||||
// Check if we have quorum for this export using ephemeral
|
||||
// signatures collected via validation messages
|
||||
auto& collector = app.getExportSignatureCollector();
|
||||
bool const hasQuorum = collector.hasQuorum(txnHash, view, app);
|
||||
auto const sigCount = collector.signatureCount(txnHash);
|
||||
|
||||
JLOG(j_.info())
|
||||
<< "[EXPORT-TIMING] TxQ: checking quorum for txn="
|
||||
<< txnHash << " exportedLgrSeq=" << exportedLgrSeq
|
||||
<< " viewSeq=" << seq << " sigCount=" << sigCount
|
||||
<< " hasQuorum=" << hasQuorum;
|
||||
|
||||
DBG_EXPORT(
|
||||
"export inject: txnHash="
|
||||
<< txnHash << " hasQuorum=" << hasQuorum << " sigCount="
|
||||
<< sigCount << " exportedLgrSeq=" << exportedLgrSeq);
|
||||
|
||||
if (hasQuorum)
|
||||
{
|
||||
DBG_EXPORT(
|
||||
"export inject: condition met! "
|
||||
<< exportedLgrSeq << " < " << (seq - 1)
|
||||
<< ", creating ttEXPORT");
|
||||
// all old entries need to be turned into Export
|
||||
// transactions so they can be removed from the directory
|
||||
"export inject: quorum reached! "
|
||||
<< sigCount << " signatures, creating ttEXPORT");
|
||||
// Quorum reached - collect signatures from memory and
|
||||
// create the ttEXPORT transaction
|
||||
|
||||
// in the previous ledger all the ExportSign transactions
|
||||
// were executed, and one-by-one added the validators'
|
||||
// signatures to the ltEXPORTED_TXN's sfSigners array. now
|
||||
// we need to collect these together and place them inside
|
||||
// the ExportedTxn blob and publish the blob in the Export
|
||||
// transaction type.
|
||||
|
||||
DBG_EXPORT("export inject: getting signers array");
|
||||
STArray signers = sleItem->getFieldArray(sfSigners);
|
||||
DBG_EXPORT("export inject: getting signers from collector");
|
||||
STArray signers = collector.getSignatures(txnHash);
|
||||
DBG_EXPORT(
|
||||
"export inject: signers count=" << signers.size());
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/AmendmentTable.h>
|
||||
#include <ripple/app/misc/ExportSignatureCollector.h>
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
#include <ripple/app/tx/impl/Change.h>
|
||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||
@@ -1134,6 +1135,9 @@ Change::applyExport()
|
||||
std::cerr
|
||||
<< "[EXPORT-TRACE] STEP-4: CLEANUP DONE (erased ltEXPORTED_TXN)"
|
||||
<< " txnID=" << txnID << std::endl;
|
||||
|
||||
// Clear ephemeral signatures from memory now that export is processed
|
||||
ctx_.app.getExportSignatureCollector().clearForTxn(txnID);
|
||||
} while (0);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -254,4 +254,135 @@ makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j)
|
||||
}
|
||||
//@@end make-export-sign-txns
|
||||
|
||||
//@@start sign-pending-exports
|
||||
std::vector<std::pair<uint256, STObject>>
|
||||
signPendingExports(
|
||||
ReadView const& view,
|
||||
Application& app,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
std::vector<std::pair<uint256, STObject>> result;
|
||||
|
||||
if (!view.rules().enabled(featureExport))
|
||||
return result;
|
||||
|
||||
JLOG(j.debug()) << "signPendingExports: started";
|
||||
|
||||
auto const seq = view.info().seq;
|
||||
|
||||
// If we're not a validator we do nothing here
|
||||
if (app.getValidationPublicKey().empty())
|
||||
return result;
|
||||
|
||||
auto const& keys = app.getValidatorKeys();
|
||||
|
||||
if (keys.configInvalid())
|
||||
return result;
|
||||
|
||||
PublicKey pkSigning = app.getValidationPublicKey();
|
||||
auto const pk = app.validatorManifests().getMasterKey(pkSigning);
|
||||
|
||||
// Only continue if we're on the UNLReport
|
||||
if (!inUNLReport(view, app, pk, j))
|
||||
return result;
|
||||
|
||||
AccountID signingAcc = calcAccountID(pkSigning);
|
||||
|
||||
Keylet const exportedDirKeylet{keylet::exportedDir()};
|
||||
if (dirIsEmpty(view, exportedDirKeylet))
|
||||
return result;
|
||||
|
||||
std::shared_ptr<SLE const> sleDirNode{};
|
||||
unsigned int uDirEntry{0};
|
||||
uint256 dirEntry{beast::zero};
|
||||
|
||||
if (!cdirFirst(
|
||||
view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry))
|
||||
return result;
|
||||
|
||||
do
|
||||
{
|
||||
Keylet const itemKeylet{ltCHILD, dirEntry};
|
||||
auto sleItem = view.read(itemKeylet);
|
||||
if (!sleItem)
|
||||
{
|
||||
JLOG(j.warn()) << "signPendingExports: directory node in ledger "
|
||||
<< seq << " has index to object that is missing: "
|
||||
<< to_string(dirEntry);
|
||||
continue;
|
||||
}
|
||||
|
||||
LedgerEntryType const nodeType{
|
||||
safe_cast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
|
||||
|
||||
if (nodeType != ltEXPORTED_TXN)
|
||||
{
|
||||
JLOG(j.warn()) << "signPendingExports: exported directory "
|
||||
"contained non ltEXPORTED_TXN type";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const& exported = const_cast<ripple::STLedgerEntry&>(*sleItem)
|
||||
.getField(sfExportedTxn)
|
||||
.downcast<STObject>();
|
||||
|
||||
auto exportedLgrSeq = sleItem->getFieldU32(sfLedgerSequence);
|
||||
|
||||
// Sign transactions that were added in the CURRENT ledger being
|
||||
// validated (This is called during validation, so we sign what's in
|
||||
// this ledger)
|
||||
if (exportedLgrSeq != seq)
|
||||
continue;
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exported.add(*s);
|
||||
SerialIter sitTrans(s->slice());
|
||||
try
|
||||
{
|
||||
auto const& stpTrans =
|
||||
std::make_shared<STTx const>(std::ref(sitTrans));
|
||||
|
||||
if (!stpTrans->isFieldPresent(sfAccount) ||
|
||||
stpTrans->getAccountID(sfAccount) == beast::zero)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "signPendingExports: sfAccount missing or zero.";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto txnHash = stpTrans->getTransactionID();
|
||||
|
||||
// Build the multisig for the exported transaction
|
||||
Serializer sigData = buildMultiSigningData(*stpTrans, signingAcc);
|
||||
auto multisig =
|
||||
ripple::sign(keys.publicKey, keys.secretKey, sigData.slice());
|
||||
|
||||
// Create the sfSigner object (same as what goes in ttEXPORT_SIGN)
|
||||
STObject signer(sfSigner);
|
||||
signer.setFieldVL(sfSigningPubKey, keys.publicKey);
|
||||
signer.setAccountID(sfAccount, signingAcc);
|
||||
signer.setFieldVL(sfTxnSignature, multisig);
|
||||
|
||||
JLOG(j.debug())
|
||||
<< "signPendingExports: signed export " << txnHash
|
||||
<< " with validator " << toBase58(TokenType::NodePublic, pk);
|
||||
|
||||
result.emplace_back(txnHash, std::move(signer));
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
JLOG(j.warn()) << "signPendingExports: Failure: " << e.what()
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
} while (
|
||||
cdirNext(view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
||||
|
||||
JLOG(j.debug()) << "signPendingExports: signed " << result.size()
|
||||
<< " exports";
|
||||
|
||||
return result;
|
||||
}
|
||||
//@@end sign-pending-exports
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -71,6 +71,24 @@ public:
|
||||
*/
|
||||
std::vector<std::shared_ptr<STTx const>>
|
||||
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j);
|
||||
|
||||
/**
|
||||
* Sign pending exports for ephemeral signature collection.
|
||||
*
|
||||
* Returns a vector of (txnHash, sfSigner) pairs for exports that need signing.
|
||||
* The signatures are NOT wrapped in ttEXPORT_SIGN transactions - they're
|
||||
* intended to be broadcast via validation messages and collected in memory.
|
||||
*
|
||||
* @param view The current ledger view
|
||||
* @param app The application (for validator keys and UNL)
|
||||
* @param j Journal for logging
|
||||
* @return Vector of (txnHash, signerObject) pairs
|
||||
*/
|
||||
std::vector<std::pair<uint256, STObject>>
|
||||
signPendingExports(
|
||||
ReadView const& view,
|
||||
Application& app,
|
||||
beast::Journal const& j);
|
||||
//@@end exportsign-class
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <ripple/app/ledger/InboundTransactions.h>
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/ledger/TransactionMaster.h>
|
||||
#include <ripple/app/misc/ExportSignatureCollector.h>
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/app/misc/LoadFeeTrack.h>
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
@@ -3194,6 +3195,46 @@ PeerImp::checkValidation(
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract export signatures from the validation message
|
||||
if (packet->exportsignatures_size() > 0)
|
||||
{
|
||||
auto const validatorPK = val->getSignerPublic();
|
||||
auto const currentSeq = val->getFieldU32(sfLedgerSequence);
|
||||
|
||||
JLOG(p_journal_.info())
|
||||
<< "[EXPORT-TIMING] PeerImp: received TMValidation with "
|
||||
<< packet->exportsignatures_size() << " export sigs from peer"
|
||||
<< " for seq=" << currentSeq;
|
||||
|
||||
for (int i = 0; i < packet->exportsignatures_size(); ++i)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const& data = packet->exportsignatures(i);
|
||||
SerialIter sit(makeSlice(data));
|
||||
uint256 txnHash = sit.getBitString<256>();
|
||||
STObject signer(sit, sfSigner);
|
||||
|
||||
JLOG(p_journal_.info()) << "[EXPORT-TIMING] PeerImp: storing "
|
||||
"PEER signature for txn="
|
||||
<< txnHash << " seq=" << currentSeq;
|
||||
|
||||
JLOG(p_journal_.debug())
|
||||
<< "Received export signature for " << txnHash
|
||||
<< " from validator "
|
||||
<< toBase58(TokenType::NodePublic, validatorPK);
|
||||
|
||||
app_.getExportSignatureCollector().addSignature(
|
||||
txnHash, validatorPK, std::move(signer), currentSeq);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(p_journal_.warn())
|
||||
<< "Failed to parse export signature: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME it should be safe to remove this try/catch. Investigate codepaths.
|
||||
try
|
||||
{
|
||||
|
||||
2
src/ripple/proto/.clang-format
Normal file
2
src/ripple/proto/.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
DisableFormat: true
|
||||
@@ -282,6 +282,11 @@ message TMValidation
|
||||
|
||||
// Number of hops traveled
|
||||
optional uint32 hops = 3 [deprecated = true];
|
||||
|
||||
// Export signatures for pending exports validated in this ledger.
|
||||
// Each entry is: txnHash (32 bytes) + serialized sfSigner STObject.
|
||||
// Used for ephemeral export signature collection via validation gossip.
|
||||
repeated bytes exportSignatures = 4;
|
||||
}
|
||||
|
||||
// An array of Endpoint messages
|
||||
|
||||
Reference in New Issue
Block a user