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:
Nicholas Dudfield
2026-01-26 17:54:17 +07:00
parent f2838351c9
commit 244a28b981
13 changed files with 669 additions and 60 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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
{

View File

@@ -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&

View 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

View 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

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
{

View File

@@ -0,0 +1,2 @@
---
DisableFormat: true

View File

@@ -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