mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-07 04:12:28 +00:00
Compare commits
32 Commits
consensus-
...
export-uvt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e3dc41d4 | ||
|
|
ce76632322 | ||
|
|
7e8e0654cd | ||
|
|
38af0626e0 | ||
|
|
8500e86f57 | ||
|
|
1fc4fd9bfd | ||
|
|
e4875e5398 | ||
|
|
5b1b142be0 | ||
|
|
5ba832204a | ||
|
|
1257b3a65c | ||
|
|
6013ed2cb6 | ||
|
|
034010716e | ||
|
|
b28793b0fa | ||
|
|
4bce392c31 | ||
|
|
244a28b981 | ||
|
|
f2838351c9 | ||
|
|
dae082d6a5 | ||
|
|
619a4a68f7 | ||
|
|
4a6db8bb05 | ||
|
|
c86479bc58 | ||
|
|
dc6a2dc6ff | ||
|
|
c01b9a657b | ||
|
|
652b181b5d | ||
|
|
8329d78f32 | ||
|
|
bf4579c1d1 | ||
|
|
73e099eb23 | ||
|
|
2e311b4259 | ||
|
|
7c8e940091 | ||
|
|
9b90c50789 | ||
|
|
a18e2cb2c6 | ||
|
|
be5f425122 | ||
|
|
fc6f4762da |
File diff suppressed because it is too large
Load Diff
@@ -47,5 +47,7 @@
|
||||
#define MEM_OVERLAP -43
|
||||
#define TOO_MANY_STATE_MODIFICATIONS -44
|
||||
#define TOO_MANY_NAMESPACES -45
|
||||
#define EXPORT_FAILURE -46
|
||||
#define TOO_MANY_EXPORTED_TXN -47
|
||||
#define HOOK_ERROR_CODES
|
||||
#endif //HOOK_ERROR_CODES
|
||||
|
||||
@@ -336,5 +336,15 @@ prepare(
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
xport_reserve(uint32_t count);
|
||||
|
||||
extern int64_t
|
||||
xport(
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
#define HOOK_EXTERN
|
||||
#endif // HOOK_EXTERN
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#define sfHookExecutionIndex ((1U << 16U) + 19U)
|
||||
#define sfHookApiVersion ((1U << 16U) + 20U)
|
||||
#define sfHookStateScale ((1U << 16U) + 21U)
|
||||
#define sfHookExportCount ((1U << 16U) + 22U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -230,6 +231,7 @@
|
||||
#define sfHookEmission ((14U << 16U) + 93U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfExportedTxn ((14U << 16U) + 90U)
|
||||
#define sfSigners ((15U << 16U) + 3U)
|
||||
#define sfSignerEntries ((15U << 16U) + 4U)
|
||||
#define sfTemplate ((15U << 16U) + 5U)
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#define ttURITOKEN_BUY 47
|
||||
#define ttURITOKEN_CREATE_SELL_OFFER 48
|
||||
#define ttURITOKEN_CANCEL_SELL_OFFER 49
|
||||
#define ttEXPORT 90
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
#define ttREMARKS_SET 94
|
||||
|
||||
@@ -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>
|
||||
@@ -35,6 +36,7 @@
|
||||
#include <ripple/app/misc/TxQ.h>
|
||||
#include <ripple/app/misc/ValidatorKeys.h>
|
||||
#include <ripple/app/misc/ValidatorList.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/basics/random.h>
|
||||
#include <ripple/beast/core/LexicalCast.h>
|
||||
#include <ripple/consensus/LedgerTiming.h>
|
||||
@@ -652,6 +654,10 @@ RCLConsensus::Adaptor::doAccept(
|
||||
tapNONE,
|
||||
"consensus",
|
||||
[&](OpenView& view, beast::Journal j) {
|
||||
// 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);
|
||||
});
|
||||
|
||||
@@ -868,9 +874,38 @@ RCLConsensus::Adaptor::validate(
|
||||
|
||||
handleNewValidation(app_, v, "local");
|
||||
|
||||
//@@start validate-sign-exports
|
||||
// Sign pending exports and collect signatures for ephemeral broadcasting
|
||||
auto exportSigs = signPendingExports(*ledger.ledger_, app_, j_);
|
||||
|
||||
// Store our own signatures in memory
|
||||
auto const currentSeq = ledger.ledger_->info().seq;
|
||||
for (auto const& [txnHash, signer] : exportSigs)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
if (!exportSigs.empty())
|
||||
{
|
||||
JLOG(j_.debug()) << "Export: broadcasting " << exportSigs.size()
|
||||
<< " signatures with validation for seq="
|
||||
<< ledger.seq();
|
||||
}
|
||||
//@@end validate-sign-exports
|
||||
app_.overlay().broadcast(val);
|
||||
|
||||
// Publish to all our subscribers:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#define uint256 std::string
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureExport "1"
|
||||
#define fix20250131 "1"
|
||||
namespace hook_api {
|
||||
struct Rules
|
||||
@@ -373,7 +374,10 @@ enum hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
EXPORT_FAILURE = -46,
|
||||
TOO_MANY_EXPORTED_TXN = -47,
|
||||
|
||||
};
|
||||
|
||||
enum ExitType : uint8_t {
|
||||
@@ -387,6 +391,7 @@ const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_export = 4;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
@@ -427,10 +432,6 @@ getImportWhitelist(Rules const& rules)
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef I32
|
||||
#undef I64
|
||||
|
||||
enum GuardRulesVersion : uint64_t {
|
||||
GuardRuleFix20250131 = 0x00000001,
|
||||
};
|
||||
|
||||
@@ -146,6 +146,7 @@ struct HookResult
|
||||
|
||||
std::queue<std::shared_ptr<ripple::Transaction>>
|
||||
emittedTxn{}; // etx stored here until accept/rollback
|
||||
std::queue<std::shared_ptr<ripple::Transaction>> exportedTxn{};
|
||||
HookStateMap& stateMap;
|
||||
uint16_t changedStateCount = 0;
|
||||
std::map<
|
||||
@@ -202,6 +203,7 @@ struct HookContext
|
||||
uint16_t ledger_nonce_counter{0};
|
||||
int64_t expected_etxn_count{-1}; // make this a 64bit int so the uint32
|
||||
// from the hookapi cant overflow it
|
||||
int64_t expected_export_count{-1};
|
||||
std::map<ripple::uint256, bool> nonce_used{};
|
||||
uint32_t generation =
|
||||
0; // used for caching, only generated when txn_generation is called
|
||||
|
||||
@@ -372,3 +372,13 @@ HOOK_API_DEFINITION(
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
featureHooksUpdate2)
|
||||
|
||||
// int64_t xport_reserve(uint32_t count);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport_reserve, (uint32_t),
|
||||
featureExport)
|
||||
|
||||
// int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
featureExport)
|
||||
|
||||
@@ -1575,6 +1575,7 @@ hook::finalizeHookResult(
|
||||
// directory) if we are allowed to
|
||||
std::vector<std::pair<uint256 /* txnid */, uint256 /* emit nonce */>>
|
||||
emission_txnid;
|
||||
std::vector<uint256 /* txnid */> exported_txnid;
|
||||
|
||||
if (doEmit)
|
||||
{
|
||||
@@ -1630,6 +1631,61 @@ hook::finalizeHookResult(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DBG_PRINTF("exported txn count: %d\n", hookResult.exportedTxn.size());
|
||||
for (; hookResult.exportedTxn.size() > 0; hookResult.exportedTxn.pop())
|
||||
{
|
||||
auto& tpTrans = hookResult.exportedTxn.front();
|
||||
auto& id = tpTrans->getID();
|
||||
JLOG(j.trace()) << "HookExport[" << HR_ACC() << "]: " << id;
|
||||
|
||||
// exported txns must be marked bad by the hash router to ensure
|
||||
// under no circumstances they will enter consensus on *this* chain.
|
||||
applyCtx.app.getHashRouter().setFlags(id, SF_BAD);
|
||||
|
||||
std::shared_ptr<const ripple::STTx> ptr =
|
||||
tpTrans->getSTransaction();
|
||||
|
||||
auto exportedId = keylet::exportedTxn(id);
|
||||
auto sleExported = applyCtx.view().peek(exportedId);
|
||||
|
||||
if (!sleExported)
|
||||
{
|
||||
exported_txnid.emplace_back(id);
|
||||
|
||||
sleExported = std::make_shared<SLE>(exportedId);
|
||||
|
||||
// RH TODO: add a new constructor to STObject to avoid this
|
||||
// serder thing
|
||||
ripple::Serializer s;
|
||||
ptr->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
|
||||
sleExported->emplace_back(ripple::STObject(sit, sfExportedTxn));
|
||||
auto page = applyCtx.view().dirInsert(
|
||||
keylet::exportedDir(), exportedId, [&](SLE::ref sle) {
|
||||
(*sle)[sfFlags] = lsfEmittedDir;
|
||||
});
|
||||
|
||||
if (page)
|
||||
{
|
||||
(*sleExported)[sfOwnerNode] = *page;
|
||||
(*sleExported)[sfLedgerSequence] =
|
||||
applyCtx.view().info().seq;
|
||||
(*sleExported)[sfTransactionHash] = id;
|
||||
applyCtx.view().insert(sleExported);
|
||||
JLOG(j.debug())
|
||||
<< "Export: created ltEXPORTED_TXN for " << id;
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "HookError[" << HR_ACC() << "]: "
|
||||
<< "Export Directory full when trying to insert " << id;
|
||||
return tecDIR_FULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool const fixV2 = applyCtx.view().rules().enabled(fixXahauV2);
|
||||
@@ -1656,6 +1712,10 @@ hook::finalizeHookResult(
|
||||
meta.setFieldU16(
|
||||
sfHookEmitCount,
|
||||
emission_txnid.size()); // this will never wrap, hard limit
|
||||
if (applyCtx.view().rules().enabled(featureExport))
|
||||
{
|
||||
meta.setFieldU16(sfHookExportCount, exported_txnid.size());
|
||||
}
|
||||
meta.setFieldU16(sfHookExecutionIndex, exec_index);
|
||||
meta.setFieldU16(sfHookStateChangeCount, hookResult.changedStateCount);
|
||||
meta.setFieldH256(sfHookHash, hookResult.hookHash);
|
||||
@@ -2857,6 +2917,27 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count)
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(int64_t, xport_reserve, uint32_t count)
|
||||
{
|
||||
HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx,
|
||||
// hookCtx on current stack
|
||||
|
||||
if (hookCtx.expected_export_count > -1)
|
||||
return ALREADY_SET;
|
||||
|
||||
if (count < 1)
|
||||
return TOO_SMALL;
|
||||
|
||||
if (count > hook_api::max_export)
|
||||
return TOO_BIG;
|
||||
|
||||
hookCtx.expected_export_count = count;
|
||||
|
||||
return count;
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// Compute the burden of an emitted transaction based on a number of factors
|
||||
DEFINE_HOOK_FUNCTION(int64_t, etxn_burden)
|
||||
{
|
||||
@@ -3920,6 +4001,95 @@ DEFINE_HOOK_FUNCTION(
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
//@@start xport-impl
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
xport,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (write_len < 32)
|
||||
return TOO_SMALL;
|
||||
|
||||
auto& app = hookCtx.applyCtx.app;
|
||||
|
||||
if (hookCtx.expected_export_count < 0)
|
||||
return PREREQUISITE_NOT_MET;
|
||||
|
||||
if (hookCtx.result.exportedTxn.size() >= hookCtx.expected_export_count)
|
||||
return TOO_MANY_EXPORTED_TXN;
|
||||
|
||||
ripple::Blob blob{memory + read_ptr, memory + read_ptr + read_len};
|
||||
|
||||
std::shared_ptr<STTx const> stpTrans;
|
||||
try
|
||||
{
|
||||
stpTrans = std::make_shared<STTx const>(
|
||||
SerialIter{memory + read_ptr, read_len});
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
JLOG(j.trace()) << "HookExport[" << HC_ACC() << "]: Failed " << e.what()
|
||||
<< "\n";
|
||||
return EXPORT_FAILURE;
|
||||
}
|
||||
|
||||
if (!stpTrans->isFieldPresent(sfAccount) ||
|
||||
stpTrans->getAccountID(sfAccount) != hookCtx.result.account)
|
||||
{
|
||||
JLOG(j.trace()) << "HookExport[" << HC_ACC()
|
||||
<< "]: Attempted to export a txn that's not for this "
|
||||
"Hook's Account ID.";
|
||||
return EXPORT_FAILURE;
|
||||
}
|
||||
|
||||
std::string reason;
|
||||
auto tpTrans = std::make_shared<Transaction>(stpTrans, reason, app);
|
||||
// RHTODO: is this needed or wise? VVV
|
||||
if (tpTrans->getStatus() != NEW)
|
||||
{
|
||||
JLOG(j.trace()) << "HookExport[" << HC_ACC()
|
||||
<< "]: tpTrans->getStatus() != NEW";
|
||||
return EXPORT_FAILURE;
|
||||
}
|
||||
auto const& txID = tpTrans->getID();
|
||||
|
||||
if (txID.size() > write_len)
|
||||
return TOO_SMALL;
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, txID.size(), memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
auto const write_txid = [&]() -> int64_t {
|
||||
WRITE_WASM_MEMORY_AND_RETURN(
|
||||
write_ptr,
|
||||
txID.size(),
|
||||
txID.data(),
|
||||
txID.size(),
|
||||
memory,
|
||||
memory_length);
|
||||
};
|
||||
|
||||
int64_t result = write_txid();
|
||||
|
||||
if (result == 32)
|
||||
hookCtx.result.exportedTxn.push(tpTrans);
|
||||
|
||||
return result;
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
//@@end xport-impl
|
||||
/*
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
|
||||
@@ -120,7 +120,9 @@ OpenLedger::accept(
|
||||
f(*next, j_);
|
||||
// Apply local tx
|
||||
for (auto const& item : locals)
|
||||
{
|
||||
app.getTxQ().apply(app, *next, item.second, flags, j_);
|
||||
}
|
||||
|
||||
// If we didn't relay this transaction recently, relay it to all peers
|
||||
for (auto const& txpair : next->txs)
|
||||
|
||||
@@ -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(),
|
||||
@@ -599,6 +604,18 @@ public:
|
||||
return validatorKeys_.publicKey;
|
||||
}
|
||||
|
||||
SecretKey const&
|
||||
getValidationSecretKey() const override
|
||||
{
|
||||
return validatorKeys_.secretKey;
|
||||
}
|
||||
|
||||
ValidatorKeys const&
|
||||
getValidatorKeys() const override
|
||||
{
|
||||
return validatorKeys_;
|
||||
}
|
||||
|
||||
NetworkOPs&
|
||||
getOPs() override
|
||||
{
|
||||
@@ -804,6 +821,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,10 @@ public:
|
||||
getAmendmentTable() = 0;
|
||||
virtual HashRouter&
|
||||
getHashRouter() = 0;
|
||||
//@@start app-export-collector
|
||||
virtual ExportSignatureCollector&
|
||||
getExportSignatureCollector() = 0;
|
||||
//@@end app-export-collector
|
||||
virtual LoadFeeTrack&
|
||||
getFeeTrack() = 0;
|
||||
virtual LoadManager&
|
||||
@@ -241,6 +246,11 @@ public:
|
||||
virtual PublicKey const&
|
||||
getValidationPublicKey() const = 0;
|
||||
|
||||
virtual SecretKey const&
|
||||
getValidationSecretKey() const = 0;
|
||||
|
||||
virtual ValidatorKeys const&
|
||||
getValidatorKeys() const = 0;
|
||||
virtual Resource::Manager&
|
||||
getResourceManager() = 0;
|
||||
virtual PathRequests&
|
||||
|
||||
273
src/ripple/app/misc/ExportSignatureCollector.h
Normal file
273
src/ripple/app/misc/ExportSignatureCollector.h
Normal file
@@ -0,0 +1,273 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Serializer.h>
|
||||
#include <ripple/protocol/UintTypes.h>
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Application;
|
||||
class ReadView;
|
||||
class ValidatorKeys;
|
||||
|
||||
/** 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 (80% UNL) is reached,
|
||||
at which point a ttEXPORT transaction can be created with all signatures.
|
||||
|
||||
Continuous broadcasting:
|
||||
========================
|
||||
Validators sign ALL pending ltEXPORTED_TXN entries every ledger. The
|
||||
collector caches signatures so validators don't need to re-sign; they
|
||||
just retrieve and re-broadcast their cached signature.
|
||||
|
||||
This ensures:
|
||||
- Late validators can contribute (sign when they come online)
|
||||
- Network partitions self-heal (signatures propagate on reconnect)
|
||||
- Node restarts recover (re-sign from ledger state, ltEXPORTED_TXN exists)
|
||||
|
||||
The ltEXPORTED_TXN in the ledger is the gatekeeper - once deleted (after
|
||||
ttEXPORT processed or export expired), signatures naturally stop being
|
||||
broadcast. The collector clears its cache when ttEXPORT is applied.
|
||||
|
||||
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;
|
||||
|
||||
/** Check if we have a signature from a specific validator.
|
||||
|
||||
Used to check if we've already signed an export (for caching).
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param validator The public key of the validator
|
||||
@return true if signature exists from this validator
|
||||
*/
|
||||
bool
|
||||
hasSignatureFrom(uint256 const& txnHash, PublicKey const& validator) const;
|
||||
|
||||
/** Get a signature from a specific validator.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param validator The public key of the validator
|
||||
@return The signer object, or std::nullopt if not found
|
||||
*/
|
||||
std::optional<STObject>
|
||||
getSignatureFrom(uint256 const& txnHash, PublicKey const& validator) 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);
|
||||
|
||||
// --- Signature verification cache ---
|
||||
|
||||
/** Stash the serialized transaction data for signature verification.
|
||||
|
||||
Called by signPendingExports() when signing, so we have the txn
|
||||
data available to verify signatures from other validators.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param txnData Serialized STTx for building verification data
|
||||
*/
|
||||
void
|
||||
stashTxnData(uint256 const& txnHash, Serializer txnData);
|
||||
|
||||
/** Verify and add a signature.
|
||||
|
||||
Verifies the signature against the cached txn data before adding.
|
||||
If txn data isn't cached yet (race), adds without verification and
|
||||
marks as unverified.
|
||||
|
||||
@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
|
||||
@return true if signature was added (regardless of verification status)
|
||||
*/
|
||||
bool
|
||||
verifyAndAddSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
STObject signer,
|
||||
LedgerIndex currentSeq);
|
||||
|
||||
/** Check if a signature has been cryptographically verified.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param validator The public key of the validator
|
||||
@return true if signature exists AND has been verified
|
||||
*/
|
||||
bool
|
||||
isSignatureVerified(uint256 const& txnHash, PublicKey const& validator)
|
||||
const;
|
||||
|
||||
/** Verify a previously-added signature that wasn't verified on add.
|
||||
|
||||
Called by Transactor as fallback when isSignatureVerified returns false.
|
||||
|
||||
@param txnHash The hash of the exported transaction
|
||||
@param validator The public key of the validator
|
||||
@return true if verification succeeded (or already verified)
|
||||
*/
|
||||
bool
|
||||
verifySignature(uint256 const& txnHash, PublicKey const& validator);
|
||||
|
||||
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_;
|
||||
|
||||
// Signature verification cache
|
||||
// Serialized STTx for building multisig verification data
|
||||
std::map<uint256, Serializer> exportedTxnData_;
|
||||
|
||||
// Which signatures have been cryptographically verified
|
||||
std::map<uint256, std::set<PublicKey>> verified_;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign pending exports for ephemeral signature collection.
|
||||
*
|
||||
* Called during validate() to sign ALL pending ltEXPORTED_TXN entries. The
|
||||
* signatures are returned as (txnHash, sfSigner) pairs to be included in the
|
||||
* TMValidation message and broadcast to peers.
|
||||
*
|
||||
* @param view The current ledger view being validated
|
||||
* @param app The application (for validator keys and UNL)
|
||||
* @param j Journal for logging
|
||||
* @return Vector of (txnHash, signerObject) pairs to broadcast
|
||||
*/
|
||||
std::vector<std::pair<uint256, STObject>>
|
||||
signPendingExports(
|
||||
ReadView const& view,
|
||||
Application& app,
|
||||
beast::Journal const& j);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -360,7 +360,8 @@ public:
|
||||
getLedgerFetchInfo() override;
|
||||
std::uint32_t
|
||||
acceptLedger(
|
||||
std::optional<std::chrono::milliseconds> consensusDelay) override;
|
||||
std::optional<std::chrono::milliseconds> consensusDelay,
|
||||
std::string const& caller = "unknown") override;
|
||||
void
|
||||
reportFeeChange() override;
|
||||
void
|
||||
@@ -3949,7 +3950,8 @@ NetworkOPsImp::unsubBook(std::uint64_t uSeq, Book const& book)
|
||||
|
||||
std::uint32_t
|
||||
NetworkOPsImp::acceptLedger(
|
||||
std::optional<std::chrono::milliseconds> consensusDelay)
|
||||
std::optional<std::chrono::milliseconds> consensusDelay,
|
||||
std::string const& caller)
|
||||
{
|
||||
// This code-path is exclusively used when the server is in standalone
|
||||
// mode via `ledger_accept`
|
||||
@@ -3963,6 +3965,7 @@ NetworkOPsImp::acceptLedger(
|
||||
// API in Consensus?
|
||||
beginConsensus(m_ledgerMaster.getClosedLedger()->info().hash);
|
||||
mConsensus.simulate(app_.timeKeeper().closeTime(), consensusDelay);
|
||||
|
||||
return m_ledgerMaster.getCurrentLedger()->info().seq;
|
||||
}
|
||||
|
||||
|
||||
@@ -219,8 +219,8 @@ public:
|
||||
*/
|
||||
virtual std::uint32_t
|
||||
acceptLedger(
|
||||
std::optional<std::chrono::milliseconds> consensusDelay =
|
||||
std::nullopt) = 0;
|
||||
std::optional<std::chrono::milliseconds> consensusDelay = std::nullopt,
|
||||
std::string const& caller = "unknown") = 0;
|
||||
|
||||
virtual void
|
||||
reportFeeChange() = 0;
|
||||
|
||||
616
src/ripple/app/misc/impl/ExportSignatureCollector.cpp
Normal file
616
src/ripple/app/misc/impl/ExportSignatureCollector.cpp
Normal file
@@ -0,0 +1,616 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/Manifest.h>
|
||||
#include <ripple/app/misc/ValidatorKeys.h>
|
||||
#include <ripple/app/misc/ValidatorList.h>
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <ripple/protocol/SField.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/Sign.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()) << "Export: 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_.trace()) << "Export: 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()) << "Export: hasQuorum check for " << 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_.info())
|
||||
<< "Export: quorum reached for " << txnHash << " ("
|
||||
<< signerMap.size() << "/" << unlSize << " signatures)";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool
|
||||
ExportSignatureCollector::hasSignatureFrom(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto txnIt = signatures_.find(txnHash);
|
||||
if (txnIt == signatures_.end())
|
||||
return false;
|
||||
|
||||
return txnIt->second.find(validator) != txnIt->second.end();
|
||||
}
|
||||
|
||||
std::optional<STObject>
|
||||
ExportSignatureCollector::getSignatureFrom(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto txnIt = signatures_.find(txnHash);
|
||||
if (txnIt == signatures_.end())
|
||||
return std::nullopt;
|
||||
|
||||
auto sigIt = txnIt->second.find(validator);
|
||||
if (sigIt == txnIt->second.end())
|
||||
return std::nullopt;
|
||||
|
||||
return sigIt->second;
|
||||
}
|
||||
|
||||
void
|
||||
ExportSignatureCollector::clearForTxn(uint256 const& txnHash)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto sigCount = signatures_.erase(txnHash);
|
||||
auto seqCount = firstSeenLedger_.erase(txnHash);
|
||||
exportedTxnData_.erase(txnHash);
|
||||
verified_.erase(txnHash);
|
||||
|
||||
if (sigCount > 0 || seqCount > 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "Export: cleared signatures for " << 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()) << "Export: cleaning up stale signatures for "
|
||||
<< txnHash
|
||||
<< " (age: " << (currentSeq - firstSeenLedger_[txnHash])
|
||||
<< " ledgers)";
|
||||
|
||||
signatures_.erase(txnHash);
|
||||
firstSeenLedger_.erase(txnHash);
|
||||
exportedTxnData_.erase(txnHash);
|
||||
verified_.erase(txnHash);
|
||||
}
|
||||
|
||||
if (!toRemove.empty())
|
||||
{
|
||||
JLOG(j_.info()) << "Export: cleaned up " << toRemove.size()
|
||||
<< " stale exports";
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ExportSignatureCollector::stashTxnData(
|
||||
uint256 const& txnHash,
|
||||
Serializer txnData)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
// Only stash if we don't already have it
|
||||
if (exportedTxnData_.find(txnHash) == exportedTxnData_.end())
|
||||
{
|
||||
exportedTxnData_.emplace(txnHash, std::move(txnData));
|
||||
JLOG(j_.trace()) << "Export: stashed txn data for " << txnHash;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ExportSignatureCollector::verifyAndAddSignature(
|
||||
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()) << "Export: first signature for " << txnHash
|
||||
<< " at ledger " << currentSeq;
|
||||
}
|
||||
|
||||
// Check if we already have this signature
|
||||
auto& signerMap = signatures_[txnHash];
|
||||
if (signerMap.find(validator) != signerMap.end())
|
||||
{
|
||||
JLOG(j_.trace()) << "Export: already have signature from "
|
||||
<< toBase58(TokenType::NodePublic, validator)
|
||||
<< " for " << txnHash;
|
||||
return true; // Already have it
|
||||
}
|
||||
|
||||
// Try to verify if we have the txn data
|
||||
bool verified = false;
|
||||
auto txnIt = exportedTxnData_.find(txnHash);
|
||||
if (txnIt != exportedTxnData_.end())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse the stashed transaction
|
||||
SerialIter sit(txnIt->second.slice());
|
||||
auto stpTrans = std::make_shared<STTx const>(std::ref(sit));
|
||||
|
||||
// Get signer account from the signer object
|
||||
auto signingAcc = signer.getAccountID(sfAccount);
|
||||
auto sigPubKey = signer.getFieldVL(sfSigningPubKey);
|
||||
auto signature = signer.getFieldVL(sfTxnSignature);
|
||||
|
||||
// Build the multisig data and verify
|
||||
Serializer sigData = buildMultiSigningData(*stpTrans, signingAcc);
|
||||
verified = ripple::verify(
|
||||
PublicKey(makeSlice(sigPubKey)),
|
||||
sigData.slice(),
|
||||
makeSlice(signature),
|
||||
true);
|
||||
|
||||
if (!verified)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "Export: signature verification FAILED for " << txnHash
|
||||
<< " from " << toBase58(TokenType::NodePublic, validator);
|
||||
return false; // Don't add invalid signature
|
||||
}
|
||||
|
||||
JLOG(j_.trace())
|
||||
<< "Export: signature verified for " << txnHash << " from "
|
||||
<< toBase58(TokenType::NodePublic, validator);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: signature verification exception for "
|
||||
<< txnHash << ": " << e.what();
|
||||
return false; // Don't add if we can't verify
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No txn data yet - add unverified (will verify later or in Transactor)
|
||||
JLOG(j_.trace()) << "Export: adding unverified signature for "
|
||||
<< txnHash << " (no txn data yet)";
|
||||
}
|
||||
|
||||
// Add the signature
|
||||
signerMap.emplace(validator, std::move(signer));
|
||||
|
||||
if (verified)
|
||||
{
|
||||
verified_[txnHash].insert(validator);
|
||||
}
|
||||
|
||||
JLOG(j_.trace()) << "Export: added signature from "
|
||||
<< toBase58(TokenType::NodePublic, validator) << " for "
|
||||
<< txnHash << " (total: " << signerMap.size()
|
||||
<< ", verified=" << verified << ")";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
ExportSignatureCollector::isSignatureVerified(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto it = verified_.find(txnHash);
|
||||
if (it == verified_.end())
|
||||
return false;
|
||||
|
||||
return it->second.find(validator) != it->second.end();
|
||||
}
|
||||
|
||||
bool
|
||||
ExportSignatureCollector::verifySignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
// Already verified?
|
||||
auto verIt = verified_.find(txnHash);
|
||||
if (verIt != verified_.end() &&
|
||||
verIt->second.find(validator) != verIt->second.end())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the signature
|
||||
auto sigIt = signatures_.find(txnHash);
|
||||
if (sigIt == signatures_.end())
|
||||
return false;
|
||||
|
||||
auto signerIt = sigIt->second.find(validator);
|
||||
if (signerIt == sigIt->second.end())
|
||||
return false;
|
||||
|
||||
// Get the txn data
|
||||
auto txnIt = exportedTxnData_.find(txnHash);
|
||||
if (txnIt == exportedTxnData_.end())
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: cannot verify signature - no txn data for "
|
||||
<< txnHash;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the stashed transaction
|
||||
SerialIter sit(txnIt->second.slice());
|
||||
auto stpTrans = std::make_shared<STTx const>(std::ref(sit));
|
||||
|
||||
// Get signer info
|
||||
auto const& signer = signerIt->second;
|
||||
auto signingAcc = signer.getAccountID(sfAccount);
|
||||
auto sigPubKey = signer.getFieldVL(sfSigningPubKey);
|
||||
auto signature = signer.getFieldVL(sfTxnSignature);
|
||||
|
||||
// Build the multisig data and verify
|
||||
Serializer sigData = buildMultiSigningData(*stpTrans, signingAcc);
|
||||
bool verified = ripple::verify(
|
||||
PublicKey(makeSlice(sigPubKey)),
|
||||
sigData.slice(),
|
||||
makeSlice(signature),
|
||||
true);
|
||||
|
||||
if (verified)
|
||||
{
|
||||
verified_[txnHash].insert(validator);
|
||||
JLOG(j_.trace())
|
||||
<< "Export: late-verified signature for " << txnHash << " from "
|
||||
<< toBase58(TokenType::NodePublic, validator);
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "Export: late signature verification FAILED for " << txnHash
|
||||
<< " from " << toBase58(TokenType::NodePublic, validator);
|
||||
}
|
||||
|
||||
return verified;
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: late verification exception for " << txnHash
|
||||
<< ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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.trace()) << "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>();
|
||||
|
||||
// Parse the exported transaction to get its hash
|
||||
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();
|
||||
|
||||
// Get the collector and stash txn data for signature verification.
|
||||
// This must happen before checking for cached signature so that
|
||||
// peer signatures can be verified against this txn data.
|
||||
auto& collector = app.getExportSignatureCollector();
|
||||
collector.stashTxnData(txnHash, *s);
|
||||
|
||||
// Check if we already have our signature cached in the collector.
|
||||
// This enables continuous broadcasting: we sign once, then keep
|
||||
// re-broadcasting our cached signature every ledger until the
|
||||
// export is finalized (ltEXPORTED_TXN deleted).
|
||||
auto cachedSig = collector.getSignatureFrom(txnHash, pkSigning);
|
||||
|
||||
if (cachedSig)
|
||||
{
|
||||
// Use cached signature - no need to re-sign
|
||||
JLOG(j.trace()) << "signPendingExports: using cached signature "
|
||||
"for "
|
||||
<< txnHash;
|
||||
result.emplace_back(txnHash, *cachedSig);
|
||||
continue;
|
||||
}
|
||||
|
||||
// First time seeing this export - sign it now
|
||||
JLOG(j.debug())
|
||||
<< "signPendingExports: signing fresh for " << txnHash;
|
||||
|
||||
// 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
|
||||
STObject signer(sfSigner);
|
||||
signer.setFieldVL(sfSigningPubKey, keys.publicKey);
|
||||
signer.setAccountID(sfAccount, signingAcc);
|
||||
signer.setFieldVL(sfTxnSignature, multisig);
|
||||
|
||||
JLOG(j.trace())
|
||||
<< "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;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -19,12 +19,15 @@
|
||||
|
||||
#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>
|
||||
#include <ripple/app/misc/ValidatorKeys.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/basics/mulDiv.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Sign.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
#include <algorithm>
|
||||
@@ -1444,7 +1447,6 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
Stop when the transaction fee level gets lower than the required fee
|
||||
level.
|
||||
*/
|
||||
|
||||
auto ledgerChanged = false;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
@@ -1539,6 +1541,191 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject exported transactions/signatures, if any
|
||||
if (view.rules().enabled(featureExport))
|
||||
{
|
||||
do
|
||||
{
|
||||
// if we're not a validator we do nothing here
|
||||
if (app.getValidationPublicKey().empty())
|
||||
break;
|
||||
|
||||
auto const& keys = app.getValidatorKeys();
|
||||
|
||||
if (keys.configInvalid())
|
||||
break;
|
||||
|
||||
// and if we're not on the UNLReport we also do nothing
|
||||
// Use inUNLReport() which has a grace period for seq < 256
|
||||
// (testing)
|
||||
if (!inUNLReport(view, app, keys.masterPublicKey, j_))
|
||||
break;
|
||||
|
||||
// execution to here means we're a validator and on the UNLReport
|
||||
|
||||
Keylet const exportedDirKeylet{keylet::exportedDir()};
|
||||
if (dirIsEmpty(view, exportedDirKeylet))
|
||||
break;
|
||||
|
||||
std::shared_ptr<SLE const> sleDirNode{};
|
||||
unsigned int uDirEntry{0};
|
||||
uint256 dirEntry{beast::zero};
|
||||
|
||||
if (!cdirFirst(
|
||||
view,
|
||||
exportedDirKeylet.key,
|
||||
sleDirNode,
|
||||
uDirEntry,
|
||||
dirEntry))
|
||||
break;
|
||||
|
||||
do
|
||||
{
|
||||
Keylet const itemKeylet{ltCHILD, dirEntry};
|
||||
auto sleItem = view.read(itemKeylet);
|
||||
if (!sleItem)
|
||||
{
|
||||
// Directory node has an invalid index. Bail out.
|
||||
JLOG(j_.warn())
|
||||
<< "ExportedTxn processing: directory node in ledger "
|
||||
<< view.seq()
|
||||
<< " has index to object that is missing: "
|
||||
<< to_string(dirEntry);
|
||||
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
LedgerEntryType const nodeType{
|
||||
safe_cast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
|
||||
|
||||
if (nodeType != ltEXPORTED_TXN)
|
||||
{
|
||||
JLOG(j_.warn()) << "ExportedTxn processing: emitted "
|
||||
"directory contained "
|
||||
"non ltEMITTED_TXN type";
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
JLOG(j_.trace()) << "Processing exported txn: " << *sleItem;
|
||||
|
||||
auto const& exported =
|
||||
const_cast<ripple::STLedgerEntry&>(*sleItem)
|
||||
.getField(sfExportedTxn)
|
||||
.downcast<STObject>();
|
||||
|
||||
auto const& txnHash = sleItem->getFieldH256(sfTransactionHash);
|
||||
|
||||
auto exportedLgrSeq = sleItem->getFieldU32(sfLedgerSequence);
|
||||
|
||||
auto const seq = view.seq();
|
||||
|
||||
if (exportedLgrSeq == seq)
|
||||
{
|
||||
// this shouldn't happen, but do nothing
|
||||
continue;
|
||||
}
|
||||
|
||||
//@@start txq-export-quorum-check
|
||||
// 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_.debug())
|
||||
<< "Export: checking quorum for txn=" << txnHash
|
||||
<< " exportedLgrSeq=" << exportedLgrSeq
|
||||
<< " viewSeq=" << seq << " sigCount=" << sigCount
|
||||
<< " hasQuorum=" << hasQuorum;
|
||||
|
||||
if (hasQuorum)
|
||||
{
|
||||
// Quorum reached - collect signatures from memory and
|
||||
// create the ttEXPORT transaction
|
||||
STArray signers = collector.getSignatures(txnHash);
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exported.add(*s);
|
||||
SerialIter sitTrans(s->slice());
|
||||
try
|
||||
{
|
||||
auto stpTrans =
|
||||
std::make_shared<STTx>(std::ref(sitTrans));
|
||||
|
||||
if (!stpTrans->isFieldPresent(sfAccount) ||
|
||||
stpTrans->getAccountID(sfAccount) == beast::zero)
|
||||
{
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
// RH TODO: should we force remove signingpubkey here?
|
||||
|
||||
stpTrans->setFieldArray(sfSigners, signers);
|
||||
|
||||
// Serialize the inner transaction and create an
|
||||
// STObject from it. sfExportedTxn is OBJECT type, not
|
||||
// VL. Use set() to replace the existing template field,
|
||||
// not emplace_back which would add a duplicate.
|
||||
ripple::Serializer exportedSer;
|
||||
stpTrans->add(exportedSer);
|
||||
SerialIter exportedSit(exportedSer.slice());
|
||||
|
||||
// Create ttEXPORT pseudo-transaction
|
||||
// Note: sfSigners is already on the inner stpTrans
|
||||
// (sfExportedTxn) ttEXPORT itself must NOT have
|
||||
// sfSigners at top level (Change::preflight rejects it)
|
||||
STTx exportTx(ttEXPORT, [&](auto& obj) {
|
||||
// Pseudo-transaction required fields
|
||||
// (Change::preflight checks)
|
||||
obj[sfAccount] = AccountID();
|
||||
// Export-specific fields
|
||||
obj.set(std::make_unique<STObject>(
|
||||
exportedSit, sfExportedTxn));
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldH256(sfTransactionHash, txnHash);
|
||||
});
|
||||
|
||||
// Record the ttEXPORT transaction (like ttCRON)
|
||||
// Cleanup happens via Change::applyExport() when
|
||||
// processed
|
||||
uint256 txID = exportTx.getTransactionID();
|
||||
|
||||
JLOG(j_.debug())
|
||||
<< "Export: injecting ttEXPORT txID=" << txID
|
||||
<< " with " << signers.size() << " signatures";
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exportTx.add(*s);
|
||||
|
||||
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
app.getHashRouter().setFlags(txID, SF_EMITTED);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
ledgerChanged = true;
|
||||
//@@end txq-export-quorum-check
|
||||
}
|
||||
|
||||
catch (std::exception& e)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "ExportedTxn Processing: Failure: " << e.what()
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
} while (cdirNext(
|
||||
view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
||||
|
||||
} while (0);
|
||||
}
|
||||
|
||||
// Inject emitted transactions if any
|
||||
if (view.rules().enabled(featureHooks))
|
||||
do
|
||||
@@ -1816,6 +2003,14 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
LedgerHash const& parentHash = view.info().parentHash;
|
||||
#if !NDEBUG
|
||||
auto const startingSize = byFee_.size();
|
||||
if (parentHash == parentHash_)
|
||||
{
|
||||
JLOG(j_.fatal()) << "TxQ::accept DOUBLE-ACCEPT DETECTED!"
|
||||
<< " seq=" << view.info().seq
|
||||
<< " parentHash=" << parentHash
|
||||
<< " parentHash_=" << parentHash_
|
||||
<< " byFee_.size()=" << byFee_.size();
|
||||
}
|
||||
assert(parentHash != parentHash_);
|
||||
parentHash_ = parentHash;
|
||||
#endif
|
||||
|
||||
@@ -23,6 +23,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/SetHook.h>
|
||||
@@ -98,6 +99,12 @@ Change::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.getTxnType() == ttEXPORT && !ctx.rules.enabled(featureExport))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: Export not enabled";
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -156,6 +163,7 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttEXPORT:
|
||||
return tesSUCCESS;
|
||||
case ttUNL_REPORT: {
|
||||
if (!ctx.tx.isFieldPresent(sfImportVLKey) ||
|
||||
@@ -211,6 +219,9 @@ Change::doApply()
|
||||
return applyEmitFailure();
|
||||
case ttUNL_REPORT:
|
||||
return applyUNLReport();
|
||||
case ttEXPORT:
|
||||
return applyExport();
|
||||
|
||||
default:
|
||||
assert(0);
|
||||
return tefFAILURE;
|
||||
@@ -1081,6 +1092,49 @@ Change::applyEmitFailure()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyExport()
|
||||
{
|
||||
uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash));
|
||||
|
||||
do
|
||||
{
|
||||
JLOG(j_.debug()) << "Export: processing ttEXPORT for " << txnID;
|
||||
|
||||
auto key = keylet::exportedTxn(txnID);
|
||||
|
||||
auto const& sle = view().peek(key);
|
||||
|
||||
if (!sle)
|
||||
{
|
||||
// most likely explanation is that this was somehow a double-up, so
|
||||
// just ignore
|
||||
JLOG(j_.warn())
|
||||
<< "Export: ttEXPORT could not find ltEXPORTED_TXN for "
|
||||
<< txnID;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!view().dirRemove(
|
||||
keylet::exportedDir(),
|
||||
sle->getFieldU64(sfOwnerNode),
|
||||
key,
|
||||
false))
|
||||
{
|
||||
JLOG(j_.fatal())
|
||||
<< "Export: ttEXPORT failed to remove directory entry for "
|
||||
<< txnID;
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
view().erase(sle);
|
||||
|
||||
// Clear ephemeral signatures from memory now that export is processed
|
||||
ctx_.app.getExportSignatureCollector().clearForTxn(txnID);
|
||||
} while (0);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyUNLModify()
|
||||
{
|
||||
|
||||
@@ -74,6 +74,9 @@ private:
|
||||
TER
|
||||
applyEmitFailure();
|
||||
|
||||
TER
|
||||
applyExport();
|
||||
|
||||
TER
|
||||
applyUNLReport();
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/hook/applyHook.h>
|
||||
#include <ripple/app/misc/Manifest.h>
|
||||
#include <ripple/app/tx/impl/Import.h>
|
||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||
@@ -40,6 +41,9 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static const uint256 shadowTicketNamespace =
|
||||
uint256::fromVoid("RESERVED NAMESPACE SHADOW TICKET");
|
||||
|
||||
TxConsequences
|
||||
Import::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -197,7 +201,8 @@ Import::preflight(PreflightContext const& ctx)
|
||||
if (!stpTrans || !meta)
|
||||
return temMALFORMED;
|
||||
|
||||
if (stpTrans->isFieldPresent(sfTicketSequence))
|
||||
if (stpTrans->isFieldPresent(sfTicketSequence) &&
|
||||
!ctx.rules.enabled(featureExport))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Import: cannot use TicketSequence XPOP.";
|
||||
return temMALFORMED;
|
||||
@@ -888,6 +893,30 @@ Import::preclaim(PreclaimContext const& ctx)
|
||||
return tefINTERNAL;
|
||||
}
|
||||
|
||||
bool const hasTicket = stpTrans->isFieldPresent(sfTicketSequence);
|
||||
|
||||
if (hasTicket)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureExport))
|
||||
return tefINTERNAL;
|
||||
|
||||
auto const acc = stpTrans->getAccountID(sfAccount);
|
||||
uint256 const seq = uint256(stpTrans->getFieldU32(sfTicketSequence));
|
||||
|
||||
// check if there is a shadow ticket, and if not we won't allow
|
||||
// the txn to pass into consensus
|
||||
|
||||
if (!ctx.view.exists(
|
||||
keylet::hookState(acc, seq, shadowTicketNamespace)))
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "Import: attempted to import a txn without shadow ticket.";
|
||||
return telSHADOW_TICKET_REQUIRED; // tel code to avoid
|
||||
// consensus/forward without
|
||||
// SF_BAD
|
||||
}
|
||||
}
|
||||
|
||||
auto const& sle = ctx.view.read(keylet::account(ctx.tx[sfAccount]));
|
||||
|
||||
auto const tt = stpTrans->getTxnType();
|
||||
@@ -928,13 +957,16 @@ Import::preclaim(PreclaimContext const& ctx)
|
||||
} while (0);
|
||||
}
|
||||
|
||||
if (sle && sle->isFieldPresent(sfImportSequence))
|
||||
if (!hasTicket)
|
||||
{
|
||||
uint32_t sleImportSequence = sle->getFieldU32(sfImportSequence);
|
||||
if (sle && sle->isFieldPresent(sfImportSequence))
|
||||
{
|
||||
uint32_t sleImportSequence = sle->getFieldU32(sfImportSequence);
|
||||
|
||||
// replay attempt
|
||||
if (sleImportSequence >= stpTrans->getFieldU32(sfSequence))
|
||||
return tefPAST_IMPORT_SEQ;
|
||||
// replay attempt
|
||||
if (sleImportSequence >= stpTrans->getFieldU32(sfSequence))
|
||||
return tefPAST_IMPORT_SEQ;
|
||||
}
|
||||
}
|
||||
|
||||
// when importing for the first time the fee must be zero
|
||||
@@ -1242,7 +1274,12 @@ Import::doApply()
|
||||
auto const id = ctx_.tx[sfAccount];
|
||||
auto sle = view().peek(keylet::account(id));
|
||||
|
||||
if (sle && sle->getFieldU32(sfImportSequence) >= importSequence)
|
||||
std::optional<uint256> ticket;
|
||||
if (stpTrans->isFieldPresent(sfTicketSequence))
|
||||
ticket = uint256(stpTrans->getFieldU32(sfTicketSequence));
|
||||
|
||||
if (sle && !ticket.has_value() &&
|
||||
sle->getFieldU32(sfImportSequence) >= importSequence)
|
||||
{
|
||||
// make double sure import seq hasn't passed
|
||||
JLOG(ctx_.journal.warn()) << "Import: ImportSequence passed";
|
||||
@@ -1335,9 +1372,26 @@ Import::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
sle->setFieldU32(sfImportSequence, importSequence);
|
||||
if (!ticket.has_value())
|
||||
sle->setFieldU32(sfImportSequence, importSequence);
|
||||
|
||||
sle->setFieldAmount(sfBalance, finalBal);
|
||||
|
||||
if (ticket.has_value())
|
||||
{
|
||||
auto sleTicket =
|
||||
view().peek(keylet::hookState(id, *ticket, shadowTicketNamespace));
|
||||
if (!sleTicket)
|
||||
return tefINTERNAL;
|
||||
|
||||
TER result =
|
||||
hook::setHookState(ctx_, id, shadowTicketNamespace, *ticket, {});
|
||||
if (result != tesSUCCESS)
|
||||
return result;
|
||||
|
||||
// RHUPTO: ticketseq billing?
|
||||
}
|
||||
|
||||
if (create)
|
||||
{
|
||||
view().insert(sle);
|
||||
|
||||
@@ -488,6 +488,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltHOOK_DEFINITION:
|
||||
case ltHOOK_STATE:
|
||||
case ltEMITTED_TXN:
|
||||
case ltEXPORTED_TXN:
|
||||
case ltNFTOKEN_PAGE:
|
||||
case ltNFTOKEN_OFFER:
|
||||
case ltURI_TOKEN:
|
||||
|
||||
@@ -441,6 +441,10 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
}
|
||||
|
||||
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
|
||||
// TODO: clarify API version history - version 1 was possibly
|
||||
// JSHooks? For now only version 0 is valid. Export APIs (xport,
|
||||
// xport_reserve) are gated by featureExport amendment via
|
||||
// rulesVersion, not by sfHookApiVersion.
|
||||
if (version != 0)
|
||||
{
|
||||
// we currently only accept api version 0
|
||||
|
||||
@@ -441,8 +441,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
// Only check fee is sufficient when the ledger is open.
|
||||
if (ctx.view.open())
|
||||
{
|
||||
auto const feeDue =
|
||||
minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags);
|
||||
auto feeDue = minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags);
|
||||
|
||||
if (feePaid < feeDue)
|
||||
{
|
||||
@@ -779,8 +778,7 @@ Transactor::apply()
|
||||
// list one, preflight will have already a flagged a failure.
|
||||
auto const sle = view().peek(keylet::account(account_));
|
||||
|
||||
// sle must exist except for transactions
|
||||
// that allow zero account. (and ttIMPORT)
|
||||
// sle must exist except for first import (account creation via ttIMPORT)
|
||||
assert(
|
||||
sle != nullptr || account_ == beast::zero ||
|
||||
view().rules().enabled(featureImport) &&
|
||||
@@ -847,16 +845,18 @@ NotTEC
|
||||
Transactor::checkSingleSign(PreclaimContext const& ctx)
|
||||
{
|
||||
// Check that the value in the signing key slot is a public key.
|
||||
auto const pkSigner = ctx.tx.getSigningPubKey();
|
||||
if (!publicKeyType(makeSlice(pkSigner)))
|
||||
auto const& pkSignerField = ctx.tx.getSigningPubKey();
|
||||
if (!publicKeyType(makeSlice(pkSignerField)))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkSingleSign: signing public key type is unknown";
|
||||
return tefBAD_AUTH; // FIXME: should be better error!
|
||||
}
|
||||
|
||||
PublicKey pkSigner{makeSlice(pkSignerField)};
|
||||
|
||||
// Look up the account.
|
||||
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
|
||||
auto const idSigner = calcAccountID(pkSigner);
|
||||
auto const idAccount = ctx.tx.getAccountID(sfAccount);
|
||||
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttEXPORT:
|
||||
return invoke_preflight_helper<Change>(ctx);
|
||||
case ttHOOK_SET:
|
||||
return invoke_preflight_helper<SetHook>(ctx);
|
||||
@@ -283,6 +284,7 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttEXPORT:
|
||||
return invoke_preclaim<Change>(ctx);
|
||||
case ttNFTOKEN_MINT:
|
||||
return invoke_preclaim<NFTokenMint>(ctx);
|
||||
@@ -374,6 +376,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttEXPORT:
|
||||
return Change::calculateBaseFee(view, tx);
|
||||
case ttNFTOKEN_MINT:
|
||||
return NFTokenMint::calculateBaseFee(view, tx);
|
||||
@@ -544,6 +547,7 @@ invoke_apply(ApplyContext& ctx)
|
||||
case ttFEE:
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEXPORT:
|
||||
case ttEMIT_FAILURE: {
|
||||
Change p(ctx);
|
||||
return p();
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#ifndef RIPPLE_LEDGER_VIEW_H_INCLUDED
|
||||
#define RIPPLE_LEDGER_VIEW_H_INCLUDED
|
||||
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/Manifest.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/beast/utility/Journal.h>
|
||||
#include <ripple/core/Config.h>
|
||||
@@ -1094,6 +1096,74 @@ trustTransferLockedBalance(
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an account (derived from a validator's master public key) is
|
||||
* in the UNLReport's ActiveValidators list.
|
||||
*/
|
||||
template <class V>
|
||||
bool
|
||||
inUNLReport(V const& view, AccountID const& id, beast::Journal const& j)
|
||||
{
|
||||
auto const seq = view.info().seq;
|
||||
static uint32_t lastLgrSeq = 0;
|
||||
static std::map<AccountID, bool> cache;
|
||||
|
||||
// for the first 256 ledgers we're just saying everyone is in the UNLReport
|
||||
// because otherwise testing is very difficult.
|
||||
if (seq < 256)
|
||||
return true;
|
||||
|
||||
if (lastLgrSeq != seq)
|
||||
{
|
||||
cache.clear();
|
||||
lastLgrSeq = seq;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cache.find(id) != cache.end())
|
||||
return cache[id];
|
||||
}
|
||||
|
||||
// Check if account is on UNLReport
|
||||
auto const unlRep = view.read(keylet::UNLReport());
|
||||
if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators))
|
||||
{
|
||||
JLOG(j.debug()) << "UNLReport missing";
|
||||
|
||||
// ensure we keep the cache invalid when in this state
|
||||
lastLgrSeq = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const& avs = unlRep->getFieldArray(sfActiveValidators);
|
||||
for (auto const& av : avs)
|
||||
{
|
||||
if (av.getAccountID(sfAccount) == id)
|
||||
return cache[id] = true;
|
||||
}
|
||||
|
||||
return cache[id] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a public key (or its master key via manifest lookup) is
|
||||
* in the UNLReport's ActiveValidators list.
|
||||
*/
|
||||
template <class V>
|
||||
bool
|
||||
inUNLReport(
|
||||
V const& view,
|
||||
Application& app,
|
||||
PublicKey const& pk,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
PublicKey uvPk = app.validatorManifests().getMasterKey(pk);
|
||||
|
||||
return inUNLReport(view, calcAccountID(pk), j) ||
|
||||
(uvPk != pk && inUNLReport(view, calcAccountID(uvPk), j));
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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,36 @@ PeerImp::checkValidation(
|
||||
return;
|
||||
}
|
||||
|
||||
//@@start peer-receive-export-sigs
|
||||
// Extract export signatures from the validation message
|
||||
if (packet->exportsignatures_size() > 0)
|
||||
{
|
||||
auto const validatorPK = val->getSignerPublic();
|
||||
auto const currentSeq = val->getFieldU32(sfLedgerSequence);
|
||||
|
||||
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);
|
||||
|
||||
// Verify and add - will verify against cached txn data if
|
||||
// available, otherwise adds unverified (verified later)
|
||||
app_.getExportSignatureCollector().verifyAndAddSignature(
|
||||
txnHash, validatorPK, std::move(signer), currentSeq);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(p_journal_.warn())
|
||||
<< "Export: failed to parse signature: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
//@@end peer-receive-export-sigs
|
||||
|
||||
// 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
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 92;
|
||||
static constexpr std::size_t numFeatures = 93;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -378,6 +378,7 @@ extern uint256 const fixInvalidTxFlags;
|
||||
extern uint256 const featureExtendedHookState;
|
||||
extern uint256 const fixCronStacking;
|
||||
extern uint256 const fixHookAPI20251128;
|
||||
extern uint256 const featureExport;
|
||||
extern uint256 const featureHookOnV2;
|
||||
extern uint256 const featureHooksUpdate2;
|
||||
} // namespace ripple
|
||||
|
||||
@@ -56,9 +56,15 @@ namespace keylet {
|
||||
Keylet const&
|
||||
emittedDir() noexcept;
|
||||
|
||||
Keylet const&
|
||||
exportedDir() noexcept;
|
||||
|
||||
Keylet
|
||||
emittedTxn(uint256 const& id) noexcept;
|
||||
|
||||
Keylet
|
||||
exportedTxn(uint256 const& id) noexcept;
|
||||
|
||||
Keylet
|
||||
hookDefinition(uint256 const& hash) noexcept;
|
||||
|
||||
|
||||
@@ -260,6 +260,8 @@ enum LedgerEntryType : std::uint16_t
|
||||
\sa keylet::emitted
|
||||
*/
|
||||
ltEMITTED_TXN = 'E',
|
||||
|
||||
ltEXPORTED_TXN = 0x4578, // Ex (exported transaction)
|
||||
};
|
||||
// clang-format off
|
||||
|
||||
@@ -318,7 +320,8 @@ enum LedgerSpecificFlags {
|
||||
// ltDIR_NODE
|
||||
lsfNFTokenBuyOffers = 0x00000001,
|
||||
lsfNFTokenSellOffers = 0x00000002,
|
||||
lsfEmittedDir = 0x00000004,
|
||||
lsfEmittedDir = 0x00000004,
|
||||
lsfExportedDir = 0x00000008,
|
||||
|
||||
// ltNFTOKEN_OFFER
|
||||
lsfSellNFToken = 0x00000001,
|
||||
|
||||
@@ -355,6 +355,7 @@ extern SF_UINT16 const sfHookEmitCount;
|
||||
extern SF_UINT16 const sfHookExecutionIndex;
|
||||
extern SF_UINT16 const sfHookApiVersion;
|
||||
extern SF_UINT16 const sfHookStateScale;
|
||||
extern SF_UINT16 const sfHookExportCount;
|
||||
|
||||
// 32-bit integers (common)
|
||||
extern SF_UINT32 const sfNetworkID;
|
||||
@@ -597,6 +598,7 @@ extern SField const sfSigner;
|
||||
extern SField const sfMajority;
|
||||
extern SField const sfDisabledValidator;
|
||||
extern SField const sfEmittedTxn;
|
||||
extern SField const sfExportedTxn;
|
||||
extern SField const sfHookExecution;
|
||||
extern SField const sfHookDefinition;
|
||||
extern SField const sfHookParameter;
|
||||
|
||||
@@ -67,6 +67,7 @@ enum TELcodes : TERUnderlyingType {
|
||||
telNON_LOCAL_EMITTED_TXN,
|
||||
telIMPORT_VL_KEY_NOT_RECOGNISED,
|
||||
telCAN_NOT_QUEUE_IMPORT,
|
||||
telSHADOW_TICKET_REQUIRED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -149,6 +149,9 @@ enum TxType : std::uint16_t
|
||||
ttURITOKEN_CREATE_SELL_OFFER = 48,
|
||||
ttURITOKEN_CANCEL_SELL_OFFER = 49,
|
||||
|
||||
/* A pseudo-txn containing an exported transaction plus signatures from the validators */
|
||||
ttEXPORT = 90,
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
ttCRON = 92,
|
||||
|
||||
|
||||
@@ -484,6 +484,7 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
HOOK_DEFINITION = 'D',
|
||||
EMITTED_TXN = 'E',
|
||||
EMITTED_DIR = 'F',
|
||||
EXPORTED_TXN = 0x4578, // Ex
|
||||
EXPORTED_DIR = 0x4564, // Ed
|
||||
NFTOKEN_OFFER = 'q',
|
||||
NFTOKEN_BUY_OFFERS = 'h',
|
||||
NFTOKEN_SELL_OFFERS = 'i',
|
||||
@@ -147,6 +149,14 @@ emittedDir() noexcept
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
exportedDir() noexcept
|
||||
{
|
||||
static Keylet const ret{
|
||||
ltDIR_NODE, indexHash(LedgerNameSpace::EXPORTED_DIR)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet
|
||||
hookStateDir(AccountID const& id, uint256 const& ns) noexcept
|
||||
{
|
||||
@@ -159,6 +169,12 @@ emittedTxn(uint256 const& id) noexcept
|
||||
return {ltEMITTED_TXN, indexHash(LedgerNameSpace::EMITTED_TXN, id)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
exportedTxn(uint256 const& id) noexcept
|
||||
{
|
||||
return {ltEXPORTED_TXN, indexHash(LedgerNameSpace::EXPORTED_TXN, id)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
hook(AccountID const& id) noexcept
|
||||
{
|
||||
|
||||
@@ -73,6 +73,7 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfHookExecutionIndex, soeREQUIRED},
|
||||
{sfHookStateChangeCount, soeREQUIRED},
|
||||
{sfHookEmitCount, soeREQUIRED},
|
||||
{sfHookExportCount, soeREQUIRED},
|
||||
{sfFlags, soeOPTIONAL}});
|
||||
|
||||
add(sfHookEmission.jsonName.c_str(),
|
||||
|
||||
@@ -382,6 +382,20 @@ LedgerFormats::LedgerFormats()
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
|
||||
//@@start lt-exported-txn-format
|
||||
// Signatures are collected ephemerally via TMValidation messages
|
||||
// (ExportSignatureCollector), not stored on-ledger.
|
||||
add(jss::ExportedTxn,
|
||||
ltEXPORTED_TXN,
|
||||
{
|
||||
{sfExportedTxn, soeOPTIONAL},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
//@@end lt-exported-txn-format
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16,
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookStateScale, "HookStateScale", UINT16, 21);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookExportCount, "HookExportCount", UINT16, 22);
|
||||
|
||||
// 32-bit integers (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1);
|
||||
@@ -363,6 +364,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfExportedTxn, "ExportedTxn", OBJECT, 90);
|
||||
|
||||
// array of objects
|
||||
// ARRAY/1 is reserved for end of array
|
||||
|
||||
@@ -615,7 +615,8 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttEXPORT;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -141,6 +141,7 @@ transResults()
|
||||
MAKE_ERROR(telNON_LOCAL_EMITTED_TXN, "Emitted transaction cannot be applied because it was not generated locally."),
|
||||
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
|
||||
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
|
||||
MAKE_ERROR(telSHADOW_TICKET_REQUIRED, "The imported transaction uses a TicketSequence but no shadow ticket exists."),
|
||||
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
|
||||
MAKE_ERROR(temBAD_AMOUNT, "Can only send positive amounts."),
|
||||
MAKE_ERROR(temBAD_CURRENCY, "Malformed: Bad currency."),
|
||||
|
||||
@@ -490,6 +490,17 @@ TxFormats::TxFormats()
|
||||
{sfStartTime, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
//@@start tt-export-format
|
||||
add(jss::Export,
|
||||
ttEXPORT,
|
||||
{
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfExportedTxn, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
//@@end tt-export-format
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -144,6 +144,8 @@ JSS(HookStateData); // field.
|
||||
JSS(HookStateKey); // field.
|
||||
JSS(EmittedTxn); // ledger type.
|
||||
JSS(EmitDetails); // field.
|
||||
JSS(ExportedTxn); // ledger type.
|
||||
JSS(Export); // transaction type.
|
||||
JSS(SignerList); // ledger type.
|
||||
JSS(SignerListSet); // transaction type.
|
||||
JSS(SigningPubKey); // field.
|
||||
|
||||
@@ -43,7 +43,7 @@ doLedgerAccept(RPC::JsonContext& context)
|
||||
else
|
||||
{
|
||||
std::unique_lock lock{context.app.getMasterMutex()};
|
||||
context.netOps.acceptLedger();
|
||||
context.netOps.acceptLedger(std::nullopt, "RPC:ledger_accept");
|
||||
jvResult[jss::ledger_current_index] =
|
||||
context.ledgerMaster.getCurrentLedgerIndex();
|
||||
}
|
||||
|
||||
318
src/test/app/Export_test.cpp
Normal file
318
src/test/app/Export_test.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/app/Export_test_hooks.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/hook.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
using TestHook = std::vector<uint8_t> const&;
|
||||
|
||||
// Large fee for hook operations
|
||||
#define HSFEE fee(100'000'000)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Per-partition debug logging for tests
|
||||
// Usage:
|
||||
// auto logs = std::make_unique<DebugLogs>(*this, DebugLogs::Levels{
|
||||
// {"View", kTrace}, // Hook operations
|
||||
// {"TxQ", kDebug}, // Transaction queue
|
||||
// });
|
||||
// Env env{*this, envconfig(), features, std::move(logs), kError};
|
||||
//------------------------------------------------------------------------------
|
||||
class DebugLogs : public Logs
|
||||
{
|
||||
public:
|
||||
using Levels = std::map<std::string, beast::severities::Severity>;
|
||||
|
||||
private:
|
||||
beast::unit_test::suite& suite_;
|
||||
Levels levels_;
|
||||
|
||||
public:
|
||||
DebugLogs(beast::unit_test::suite& suite, Levels levels = {})
|
||||
: Logs(beast::severities::kError)
|
||||
, suite_(suite)
|
||||
, levels_(std::move(levels))
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<beast::Journal::Sink>
|
||||
makeSink(
|
||||
std::string const& partition,
|
||||
beast::severities::Severity defaultThresh) override
|
||||
{
|
||||
auto thresh = defaultThresh;
|
||||
if (auto it = levels_.find(partition); it != levels_.end())
|
||||
thresh = it->second;
|
||||
return std::make_unique<SuiteJournalSink>(partition, thresh, suite_);
|
||||
}
|
||||
};
|
||||
|
||||
struct Export_test : public beast::unit_test::suite
|
||||
{
|
||||
// Hook that exports a payment using xport (for cross-chain export)
|
||||
// xport APIs are gated by featureExport amendment, not sfHookApiVersion
|
||||
TestHook xport_wasm = export_test_wasm[R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t xport_reserve(uint32_t count);
|
||||
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
|
||||
extern int64_t otxn_type(void);
|
||||
extern int64_t ledger_seq(void);
|
||||
|
||||
#define SBUF(x) (uint32_t)(x), sizeof(x)
|
||||
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
|
||||
|
||||
#define ttPAYMENT 0
|
||||
#define tfCANONICAL 0x80000000UL
|
||||
|
||||
#define amAMOUNT 1
|
||||
#define amFEE 8
|
||||
#define atACCOUNT 1
|
||||
#define atDESTINATION 3
|
||||
|
||||
#define ENCODE_TT(buf_out, tt) \
|
||||
buf_out[0] = 0x12U; \
|
||||
buf_out[1] = (tt >> 8) & 0xFFU; \
|
||||
buf_out[2] = tt & 0xFFU; \
|
||||
buf_out += 3;
|
||||
|
||||
#define ENCODE_FLAGS(buf_out, flags) \
|
||||
buf_out[0] = 0x22U; \
|
||||
buf_out[1] = (flags >> 24) & 0xFFU; \
|
||||
buf_out[2] = (flags >> 16) & 0xFFU; \
|
||||
buf_out[3] = (flags >> 8) & 0xFFU; \
|
||||
buf_out[4] = flags & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
#define ENCODE_SEQUENCE(buf_out, seq) \
|
||||
buf_out[0] = 0x24U; \
|
||||
buf_out[1] = (seq >> 24) & 0xFFU; \
|
||||
buf_out[2] = (seq >> 16) & 0xFFU; \
|
||||
buf_out[3] = (seq >> 8) & 0xFFU; \
|
||||
buf_out[4] = seq & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
#define ENCODE_FLS(buf_out, fls) \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = 0x1AU; \
|
||||
buf_out[2] = (fls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (fls >> 16) & 0xFFU; \
|
||||
buf_out[4] = (fls >> 8) & 0xFFU; \
|
||||
buf_out[5] = fls & 0xFFU; \
|
||||
buf_out += 6;
|
||||
|
||||
#define ENCODE_LLS(buf_out, lls) \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = 0x1BU; \
|
||||
buf_out[2] = (lls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (lls >> 16) & 0xFFU; \
|
||||
buf_out[4] = (lls >> 8) & 0xFFU; \
|
||||
buf_out[5] = lls & 0xFFU; \
|
||||
buf_out += 6;
|
||||
|
||||
#define ENCODE_DROPS(buf_out, drops, amt_type) \
|
||||
buf_out[0] = 0x60U + amt_type; \
|
||||
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
|
||||
buf_out[2] = (drops >> 48) & 0xFFU; \
|
||||
buf_out[3] = (drops >> 40) & 0xFFU; \
|
||||
buf_out[4] = (drops >> 32) & 0xFFU; \
|
||||
buf_out[5] = (drops >> 24) & 0xFFU; \
|
||||
buf_out[6] = (drops >> 16) & 0xFFU; \
|
||||
buf_out[7] = (drops >> 8) & 0xFFU; \
|
||||
buf_out[8] = drops & 0xFFU; \
|
||||
buf_out += 9;
|
||||
|
||||
#define ENCODE_SIGNING_PUBKEY_NULL(buf_out) \
|
||||
buf_out[0] = 0x73U; \
|
||||
buf_out[1] = 0x21U; \
|
||||
for (int i = 2; i < 35; ++i) buf_out[i] = 0; \
|
||||
buf_out += 35;
|
||||
|
||||
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
|
||||
buf_out[0] = 0x80U + acc_type; \
|
||||
buf_out[1] = 0x14U; \
|
||||
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
|
||||
buf_out += 22;
|
||||
|
||||
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
|
||||
// Only trigger on Payment transactions
|
||||
if (otxn_type() != ttPAYMENT)
|
||||
return accept(0, 0, 0);
|
||||
|
||||
// Reserve 1 xport slot
|
||||
ASSERT(xport_reserve(1) == 1);
|
||||
|
||||
// Get destination from parameter "DST"
|
||||
uint8_t dst[20];
|
||||
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
|
||||
ASSERT(dst_len == 20);
|
||||
|
||||
// Get hook account (source) - xport requires sfAccount to match hook account
|
||||
uint8_t acc[20];
|
||||
ASSERT(hook_account(SBUF(acc)) == 20);
|
||||
|
||||
// Get ledger seq for FLS/LLS
|
||||
uint32_t cls = (uint32_t)ledger_seq();
|
||||
|
||||
// Build payment transaction for export
|
||||
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
|
||||
uint8_t* buf = tx;
|
||||
|
||||
ENCODE_TT(buf, ttPAYMENT);
|
||||
ENCODE_FLAGS(buf, tfCANONICAL);
|
||||
ENCODE_SEQUENCE(buf, 0);
|
||||
ENCODE_FLS(buf, cls + 1);
|
||||
ENCODE_LLS(buf, cls + 5);
|
||||
|
||||
uint64_t drops = 1000000; // 1 XRP
|
||||
ENCODE_DROPS(buf, drops, amAMOUNT);
|
||||
ENCODE_DROPS(buf, 10, amFEE); // minimal fee for exported txn
|
||||
|
||||
ENCODE_SIGNING_PUBKEY_NULL(buf);
|
||||
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
|
||||
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
|
||||
|
||||
// Export!
|
||||
uint8_t hash[32];
|
||||
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
|
||||
ASSERT(xport_result == 32);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
// Helper: run xport test with given config
|
||||
// Returns true if the exported directory is empty after the flow
|
||||
// (meaning ttEXPORT cleaned up the entry)
|
||||
void
|
||||
runXportTest(
|
||||
FeatureBitset features,
|
||||
std::function<std::unique_ptr<Config>()> makeConfig,
|
||||
bool expectCleanup)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace beast::severities;
|
||||
|
||||
auto logs = std::make_unique<DebugLogs>(
|
||||
*this,
|
||||
DebugLogs::Levels{
|
||||
{"View", kTrace},
|
||||
{"TxQ", kTrace},
|
||||
});
|
||||
|
||||
Env env{*this, makeConfig(), features, std::move(logs), kError};
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Install xport hook on alice
|
||||
env(ripple::test::jtx::hook(alice, {{hso(xport_wasm)}}, 0),
|
||||
HSFEE,
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const xportLedgerSeq = env.current()->seq();
|
||||
|
||||
// Trigger hook with payment containing DST parameter
|
||||
Json::Value params(Json::arrayValue);
|
||||
Json::Value param;
|
||||
param[jss::HookParameter] = Json::Value(Json::objectValue);
|
||||
param[jss::HookParameter][jss::HookParameterName] =
|
||||
strHex(std::string("DST"));
|
||||
param[jss::HookParameter][jss::HookParameterValue] = strHex(carol.id());
|
||||
params.append(param);
|
||||
|
||||
env(pay(bob, alice, XRP(100)),
|
||||
fee(XRP(1)),
|
||||
json(jss::HookParameters, params),
|
||||
ter(tesSUCCESS));
|
||||
env.close(); // Ledger N: xport() creates ltEXPORTED_TXN
|
||||
|
||||
// Verify ltEXPORTED_TXN was created
|
||||
{
|
||||
auto const exportedDirKey = keylet::exportedDir();
|
||||
BEAST_EXPECT(env.current()->read(exportedDirKey));
|
||||
BEAST_EXPECT(!dirIsEmpty(*env.current(), exportedDirKey));
|
||||
}
|
||||
|
||||
// Close additional ledgers for signing flow
|
||||
env.close(); // N+1: validators sign via TMValidation
|
||||
env.close(); // N+2: ttEXPORT created (rawTxInsert)
|
||||
env.close(); // N+3: does ttEXPORT get applied here?
|
||||
|
||||
// Check if cleanup happened
|
||||
{
|
||||
auto const exportedDirKey = keylet::exportedDir();
|
||||
bool dirEmpty = dirIsEmpty(*env.current(), exportedDirKey);
|
||||
BEAST_EXPECT(dirEmpty == expectCleanup);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(env.current()->seq() == xportLedgerSeq + 4);
|
||||
}
|
||||
|
||||
void
|
||||
testXportPaymentWithValidator(FeatureBitset features)
|
||||
{
|
||||
testcase("Xport Payment (with validator)");
|
||||
|
||||
// With validator config, full flow should work:
|
||||
// N: xport creates entry
|
||||
// N+1: validator signs
|
||||
// N+2: ttEXPORT cleans up
|
||||
runXportTest(
|
||||
features,
|
||||
[]() { return jtx::envconfig(jtx::validator, ""); },
|
||||
true);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{supported_amendments()};
|
||||
FeatureBitset const allWithExport{all | featureExport};
|
||||
testXportPaymentWithValidator(allWithExport);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Export, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
254
src/test/app/Export_test_hooks.h
Normal file
254
src/test/app/Export_test_hooks.h
Normal file
@@ -0,0 +1,254 @@
|
||||
|
||||
// This file is generated by build_test_hooks.py
|
||||
#ifndef EXPORT_TEST_WASM_INCLUDED
|
||||
#define EXPORT_TEST_WASM_INCLUDED
|
||||
#include <map>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
std::map<std::string, std::vector<uint8_t>> export_test_wasm = {
|
||||
/* ==== WASM: 0 ==== */
|
||||
{R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t xport_reserve(uint32_t count);
|
||||
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
|
||||
extern int64_t otxn_type(void);
|
||||
extern int64_t ledger_seq(void);
|
||||
|
||||
#define SBUF(x) (uint32_t)(x), sizeof(x)
|
||||
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
|
||||
|
||||
#define ttPAYMENT 0
|
||||
#define tfCANONICAL 0x80000000UL
|
||||
|
||||
#define amAMOUNT 1
|
||||
#define amFEE 8
|
||||
#define atACCOUNT 1
|
||||
#define atDESTINATION 3
|
||||
|
||||
#define ENCODE_TT(buf_out, tt) \
|
||||
buf_out[0] = 0x12U; \
|
||||
buf_out[1] = (tt >> 8) & 0xFFU; \
|
||||
buf_out[2] = tt & 0xFFU; \
|
||||
buf_out += 3;
|
||||
|
||||
#define ENCODE_FLAGS(buf_out, flags) \
|
||||
buf_out[0] = 0x22U; \
|
||||
buf_out[1] = (flags >> 24) & 0xFFU; \
|
||||
buf_out[2] = (flags >> 16) & 0xFFU; \
|
||||
buf_out[3] = (flags >> 8) & 0xFFU; \
|
||||
buf_out[4] = flags & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
#define ENCODE_SEQUENCE(buf_out, seq) \
|
||||
buf_out[0] = 0x24U; \
|
||||
buf_out[1] = (seq >> 24) & 0xFFU; \
|
||||
buf_out[2] = (seq >> 16) & 0xFFU; \
|
||||
buf_out[3] = (seq >> 8) & 0xFFU; \
|
||||
buf_out[4] = seq & 0xFFU; \
|
||||
buf_out += 5;
|
||||
|
||||
#define ENCODE_FLS(buf_out, fls) \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = 0x1AU; \
|
||||
buf_out[2] = (fls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (fls >> 16) & 0xFFU; \
|
||||
buf_out[4] = (fls >> 8) & 0xFFU; \
|
||||
buf_out[5] = fls & 0xFFU; \
|
||||
buf_out += 6;
|
||||
|
||||
#define ENCODE_LLS(buf_out, lls) \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = 0x1BU; \
|
||||
buf_out[2] = (lls >> 24) & 0xFFU; \
|
||||
buf_out[3] = (lls >> 16) & 0xFFU; \
|
||||
buf_out[4] = (lls >> 8) & 0xFFU; \
|
||||
buf_out[5] = lls & 0xFFU; \
|
||||
buf_out += 6;
|
||||
|
||||
#define ENCODE_DROPS(buf_out, drops, amt_type) \
|
||||
buf_out[0] = 0x60U + amt_type; \
|
||||
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
|
||||
buf_out[2] = (drops >> 48) & 0xFFU; \
|
||||
buf_out[3] = (drops >> 40) & 0xFFU; \
|
||||
buf_out[4] = (drops >> 32) & 0xFFU; \
|
||||
buf_out[5] = (drops >> 24) & 0xFFU; \
|
||||
buf_out[6] = (drops >> 16) & 0xFFU; \
|
||||
buf_out[7] = (drops >> 8) & 0xFFU; \
|
||||
buf_out[8] = drops & 0xFFU; \
|
||||
buf_out += 9;
|
||||
|
||||
#define ENCODE_SIGNING_PUBKEY_NULL(buf_out) \
|
||||
buf_out[0] = 0x73U; \
|
||||
buf_out[1] = 0x21U; \
|
||||
for (int i = 2; i < 35; ++i) buf_out[i] = 0; \
|
||||
buf_out += 35;
|
||||
|
||||
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
|
||||
buf_out[0] = 0x80U + acc_type; \
|
||||
buf_out[1] = 0x14U; \
|
||||
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
|
||||
buf_out += 22;
|
||||
|
||||
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
|
||||
// Only trigger on Payment transactions
|
||||
if (otxn_type() != ttPAYMENT)
|
||||
return accept(0, 0, 0);
|
||||
|
||||
// Reserve 1 xport slot
|
||||
ASSERT(xport_reserve(1) == 1);
|
||||
|
||||
// Get destination from parameter "DST"
|
||||
uint8_t dst[20];
|
||||
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
|
||||
ASSERT(dst_len == 20);
|
||||
|
||||
// Get hook account (source) - xport requires sfAccount to match hook account
|
||||
uint8_t acc[20];
|
||||
ASSERT(hook_account(SBUF(acc)) == 20);
|
||||
|
||||
// Get ledger seq for FLS/LLS
|
||||
uint32_t cls = (uint32_t)ledger_seq();
|
||||
|
||||
// Build payment transaction for export
|
||||
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
|
||||
uint8_t* buf = tx;
|
||||
|
||||
ENCODE_TT(buf, ttPAYMENT);
|
||||
ENCODE_FLAGS(buf, tfCANONICAL);
|
||||
ENCODE_SEQUENCE(buf, 0);
|
||||
ENCODE_FLS(buf, cls + 1);
|
||||
ENCODE_LLS(buf, cls + 5);
|
||||
|
||||
uint64_t drops = 1000000; // 1 XRP
|
||||
ENCODE_DROPS(buf, drops, amAMOUNT);
|
||||
ENCODE_DROPS(buf, 10, amFEE); // minimal fee for exported txn
|
||||
|
||||
ENCODE_SIGNING_PUBKEY_NULL(buf);
|
||||
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
|
||||
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
|
||||
|
||||
// Export!
|
||||
uint8_t hash[32];
|
||||
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
|
||||
ASSERT(xport_result == 32);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
}
|
||||
)[test.hook]",
|
||||
{
|
||||
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x25U,
|
||||
0x06U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x00U, 0x01U,
|
||||
0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U,
|
||||
0x7FU, 0x01U, 0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
|
||||
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x8BU, 0x01U,
|
||||
0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U,
|
||||
0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU,
|
||||
0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U,
|
||||
0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U, 0x03U,
|
||||
0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU,
|
||||
0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x03U, 0x03U,
|
||||
0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U,
|
||||
0x63U, 0x6BU, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6FU,
|
||||
0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU, 0x00U,
|
||||
0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU,
|
||||
0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x05U,
|
||||
0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U,
|
||||
0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU,
|
||||
0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x04U, 0x03U,
|
||||
0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x21U,
|
||||
0x05U, 0x7FU, 0x01U, 0x41U, 0xE0U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U,
|
||||
0x41U, 0xD9U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU,
|
||||
0x7FU, 0x00U, 0x41U, 0xE0U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U,
|
||||
0x80U, 0x08U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU,
|
||||
0x6BU, 0x00U, 0x09U, 0x0AU, 0xF9U, 0x84U, 0x00U, 0x01U, 0xF5U, 0x84U,
|
||||
0x00U, 0x03U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x7FU, 0x23U, 0x80U,
|
||||
0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0xF0U, 0x02U, 0x6BU, 0x22U, 0x01U,
|
||||
0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U,
|
||||
0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, 0x02U,
|
||||
0x40U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x50U, 0x0DU, 0x00U,
|
||||
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U,
|
||||
0x80U, 0x00U, 0x21U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x02U, 0x40U, 0x41U,
|
||||
0x01U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x01U, 0x51U,
|
||||
0x0DU, 0x00U, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x16U,
|
||||
0x42U, 0xE2U, 0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
|
||||
0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U,
|
||||
0x14U, 0x41U, 0x96U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x03U, 0x10U,
|
||||
0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U,
|
||||
0x41U, 0x9AU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x0EU, 0x42U, 0xE7U,
|
||||
0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
|
||||
0x40U, 0x20U, 0x01U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x14U, 0x10U,
|
||||
0x86U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U,
|
||||
0x41U, 0xA8U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x1EU, 0x42U, 0xEBU,
|
||||
0x00U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x10U,
|
||||
0x87U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x02U, 0x20U, 0x01U, 0x41U,
|
||||
0xC8U, 0x00U, 0x6AU, 0x41U, 0x00U, 0x3BU, 0x01U, 0x00U, 0x20U, 0x01U,
|
||||
0x41U, 0xC0U, 0x00U, 0x3AU, 0x00U, 0x43U, 0x20U, 0x01U, 0x42U, 0x80U,
|
||||
0x80U, 0x80U, 0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U,
|
||||
0x00U, 0x3BU, 0x20U, 0x01U, 0x41U, 0xE1U, 0x80U, 0x01U, 0x3BU, 0x00U,
|
||||
0x39U, 0x20U, 0x01U, 0x41U, 0xA0U, 0x36U, 0x3BU, 0x00U, 0x33U, 0x20U,
|
||||
0x01U, 0x41U, 0xA0U, 0x34U, 0x3BU, 0x00U, 0x2DU, 0x20U, 0x01U, 0x41U,
|
||||
0x00U, 0x36U, 0x00U, 0x29U, 0x20U, 0x01U, 0x41U, 0x24U, 0x3AU, 0x00U,
|
||||
0x28U, 0x20U, 0x01U, 0x42U, 0x92U, 0x80U, 0x80U, 0x90U, 0x82U, 0x10U,
|
||||
0x37U, 0x03U, 0x20U, 0x20U, 0x01U, 0x41U, 0x00U, 0x36U, 0x02U, 0x44U,
|
||||
0x20U, 0x01U, 0x20U, 0x02U, 0xA7U, 0x22U, 0x03U, 0x41U, 0x05U, 0x6AU,
|
||||
0x22U, 0x04U, 0x3AU, 0x00U, 0x38U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U,
|
||||
0x08U, 0x76U, 0x3AU, 0x00U, 0x37U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U,
|
||||
0x10U, 0x76U, 0x3AU, 0x00U, 0x36U, 0x20U, 0x01U, 0x20U, 0x04U, 0x41U,
|
||||
0x18U, 0x76U, 0x3AU, 0x00U, 0x35U, 0x20U, 0x01U, 0x20U, 0x03U, 0x41U,
|
||||
0x01U, 0x6AU, 0x22U, 0x04U, 0x3AU, 0x00U, 0x32U, 0x20U, 0x01U, 0x20U,
|
||||
0x04U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0x31U, 0x20U, 0x01U, 0x20U,
|
||||
0x04U, 0x41U, 0x10U, 0x76U, 0x3AU, 0x00U, 0x30U, 0x20U, 0x01U, 0x20U,
|
||||
0x04U, 0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0x2FU, 0x20U, 0x01U, 0x41U,
|
||||
0xD5U, 0x00U, 0x6AU, 0x42U, 0x00U, 0x37U, 0x00U, 0x00U, 0x20U, 0x01U,
|
||||
0x41U, 0xDDU, 0x00U, 0x6AU, 0x42U, 0x00U, 0x37U, 0x00U, 0x00U, 0x20U,
|
||||
0x01U, 0x41U, 0xE5U, 0x00U, 0x6AU, 0x42U, 0x00U, 0x37U, 0x00U, 0x00U,
|
||||
0x20U, 0x01U, 0x41U, 0xEDU, 0x00U, 0x6AU, 0x41U, 0x00U, 0x3AU, 0x00U,
|
||||
0x00U, 0x20U, 0x01U, 0x41U, 0xF8U, 0x00U, 0x6AU, 0x20U, 0x01U, 0x29U,
|
||||
0x03U, 0xB8U, 0x02U, 0x37U, 0x03U, 0x00U, 0x20U, 0x01U, 0x41U, 0x80U,
|
||||
0x01U, 0x6AU, 0x20U, 0x01U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x10U,
|
||||
0x6AU, 0x28U, 0x02U, 0x00U, 0x36U, 0x02U, 0x00U, 0x20U, 0x01U, 0x41U,
|
||||
0x8EU, 0x01U, 0x6AU, 0x20U, 0x01U, 0x29U, 0x03U, 0xD8U, 0x02U, 0x37U,
|
||||
0x01U, 0x00U, 0x20U, 0x01U, 0x41U, 0x96U, 0x01U, 0x6AU, 0x20U, 0x01U,
|
||||
0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U, 0x10U, 0x6AU, 0x28U, 0x02U, 0x00U,
|
||||
0x36U, 0x01U, 0x00U, 0x20U, 0x01U, 0x41U, 0x21U, 0x3AU, 0x00U, 0x4CU,
|
||||
0x20U, 0x01U, 0x41U, 0x8AU, 0xE6U, 0x01U, 0x3BU, 0x01U, 0x4AU, 0x20U,
|
||||
0x01U, 0x42U, 0x00U, 0x37U, 0x00U, 0x4DU, 0x20U, 0x01U, 0x41U, 0x81U,
|
||||
0x29U, 0x3BU, 0x01U, 0x6EU, 0x20U, 0x01U, 0x41U, 0x83U, 0x29U, 0x3BU,
|
||||
0x01U, 0x84U, 0x01U, 0x20U, 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0xB0U,
|
||||
0x02U, 0x37U, 0x03U, 0x70U, 0x20U, 0x01U, 0x20U, 0x01U, 0x29U, 0x03U,
|
||||
0xD0U, 0x02U, 0x37U, 0x01U, 0x86U, 0x01U, 0x02U, 0x40U, 0x20U, 0x01U,
|
||||
0x41U, 0x20U, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU, 0x41U, 0xFAU, 0x00U,
|
||||
0x10U, 0x88U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU,
|
||||
0x00U, 0x41U, 0xC6U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x13U, 0x42U,
|
||||
0x85U, 0x01U, 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU,
|
||||
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U,
|
||||
0x80U, 0x00U, 0x21U, 0x02U, 0x0BU, 0x20U, 0x01U, 0x41U, 0xF0U, 0x02U,
|
||||
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU,
|
||||
0x0BU, 0x60U, 0x01U, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x59U, 0x78U,
|
||||
0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U,
|
||||
0x76U, 0x65U, 0x28U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U,
|
||||
0x00U, 0x44U, 0x53U, 0x54U, 0x00U, 0x64U, 0x73U, 0x74U, 0x5FU, 0x6CU,
|
||||
0x65U, 0x6EU, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x68U,
|
||||
0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU,
|
||||
0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x61U, 0x63U, 0x63U,
|
||||
0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x78U,
|
||||
0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x75U, 0x6CU,
|
||||
0x74U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
|
||||
}},
|
||||
|
||||
};
|
||||
}
|
||||
} // namespace ripple
|
||||
#endif
|
||||
@@ -130,7 +130,8 @@ Env::close(
|
||||
// Go through the rpc interface unless we need to simulate
|
||||
// a specific consensus delay.
|
||||
if (consensusDelay)
|
||||
app().getOPs().acceptLedger(consensusDelay);
|
||||
app().getOPs().acceptLedger(
|
||||
consensusDelay, "Env::close(consensusDelay)");
|
||||
else
|
||||
{
|
||||
auto resp = rpc("ledger_accept");
|
||||
|
||||
Reference in New Issue
Block a user