diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index a9adf2563..72200fda9 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -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 diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 2dbb0eec2..d42cb1e91 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -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(); - 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: diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index bc1d5332d..835eeda30 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -218,6 +219,7 @@ public: std::unique_ptr m_amendmentTable; std::unique_ptr mFeeTrack; std::unique_ptr hashRouter_; + std::unique_ptr exportSignatureCollector_; RCLValidations mValidations; std::unique_ptr m_loadManager; std::unique_ptr txQ_; @@ -462,6 +464,9 @@ public: stopwatch(), HashRouter::getDefaultHoldTime())) + , exportSignatureCollector_(std::make_unique( + logs_->journal("ExportSignatureCollector"))) + , mValidations( ValidationParms(), stopwatch(), @@ -818,6 +823,12 @@ public: return *hashRouter_; } + ExportSignatureCollector& + getExportSignatureCollector() override + { + return *exportSignatureCollector_; + } + RCLValidations& getValidations() override { diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 58e6a899b..1c8d4e29f 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -66,6 +66,7 @@ using SLE = STLedgerEntry; using CachedSLEs = TaggedCache; 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& diff --git a/src/ripple/app/misc/ExportSignatureCollector.h b/src/ripple/app/misc/ExportSignatureCollector.h new file mode 100644 index 000000000..bfe809459 --- /dev/null +++ b/src/ripple/app/misc/ExportSignatureCollector.h @@ -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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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 + getExportsWithQuorum(ReadView const& view, Application& app) const; + + /** Get all pending export hashes. + + @return Vector of all txnHashes being tracked + */ + std::vector + 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> + std::map> signatures_; + + // Track when each export was first seen (for timeout) + std::map 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 diff --git a/src/ripple/app/misc/impl/ExportSignatureCollector.cpp b/src/ripple/app/misc/impl/ExportSignatureCollector.cpp new file mode 100644 index 000000000..7621446c1 --- /dev/null +++ b/src/ripple/app/misc/impl/ExportSignatureCollector.cpp @@ -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 +#include +#include +#include +#include +#include + +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 +ExportSignatureCollector::getExportsWithQuorum( + ReadView const& view, + Application& app) const +{ + std::lock_guard lock(mutex_); + + std::vector 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 +ExportSignatureCollector::getPendingExports() const +{ + std::lock_guard lock(mutex_); + + std::vector 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 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 diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index a9468d4d3..d74b77aca 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -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()); diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index f077952f0..337a7c831 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -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; } diff --git a/src/ripple/app/tx/impl/ExportSign.cpp b/src/ripple/app/tx/impl/ExportSign.cpp index 3f668c441..0440d44ca 100644 --- a/src/ripple/app/tx/impl/ExportSign.cpp +++ b/src/ripple/app/tx/impl/ExportSign.cpp @@ -254,4 +254,135 @@ makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j) } //@@end make-export-sign-txns +//@@start sign-pending-exports +std::vector> +signPendingExports( + ReadView const& view, + Application& app, + beast::Journal const& j) +{ + std::vector> 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 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((*sleItem)[sfLedgerEntryType])}; + + if (nodeType != ltEXPORTED_TXN) + { + JLOG(j.warn()) << "signPendingExports: exported directory " + "contained non ltEXPORTED_TXN type"; + continue; + } + + auto const& exported = const_cast(*sleItem) + .getField(sfExportedTxn) + .downcast(); + + 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(); + exported.add(*s); + SerialIter sitTrans(s->slice()); + try + { + auto const& stpTrans = + std::make_shared(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 diff --git a/src/ripple/app/tx/impl/ExportSign.h b/src/ripple/app/tx/impl/ExportSign.h index 6f201bb2b..77e9fe0a4 100644 --- a/src/ripple/app/tx/impl/ExportSign.h +++ b/src/ripple/app/tx/impl/ExportSign.h @@ -71,6 +71,24 @@ public: */ std::vector> 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> +signPendingExports( + ReadView const& view, + Application& app, + beast::Journal const& j); //@@end exportsign-class } // namespace ripple diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index adb05ddf6..a084d3b74 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -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 { diff --git a/src/ripple/proto/.clang-format b/src/ripple/proto/.clang-format new file mode 100644 index 000000000..46626920c --- /dev/null +++ b/src/ripple/proto/.clang-format @@ -0,0 +1,2 @@ +--- +DisableFormat: true diff --git a/src/ripple/proto/ripple.proto b/src/ripple/proto/ripple.proto index 74cbfe8f6..86be162bf 100644 --- a/src/ripple/proto/ripple.proto +++ b/src/ripple/proto/ripple.proto @@ -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