Compare commits

...

32 Commits

Author SHA1 Message Date
Nicholas Dudfield
a9e3dc41d4 fix: add featureExport stub for standalone guard_checker build 2026-02-20 10:05:58 +07:00
Nicholas Dudfield
ce76632322 Merge remote-tracking branch 'origin/dev' into export-uvtxn
# Conflicts:
#	Builds/CMake/RippledCore.cmake
#	hook/extern.h
#	src/ripple/app/hook/Guard.h
#	src/ripple/app/hook/applyHook.h
#	src/ripple/app/hook/guard_checker.cpp
#	src/ripple/app/tx/impl/Change.cpp
#	src/ripple/app/tx/impl/SetHook.cpp
#	src/ripple/protocol/Feature.h
#	src/ripple/protocol/impl/Feature.cpp
#	src/ripple/protocol/jss.h
2026-02-19 10:12:55 +07:00
Nicholas Dudfield
7e8e0654cd chore: add documentation markers for pr-description-outline 2026-01-28 15:00:14 +07:00
Nicholas Dudfield
38af0626e0 chore: add documentation markers for pr-description 2026-01-28 14:50:56 +07:00
Nicholas Dudfield
8500e86f57 chore: remove projected-source documentation markers 2026-01-28 11:30:47 +07:00
Nicholas Dudfield
1fc4fd9bfd chore: regenerate hook headers for export feature 2026-01-28 11:27:13 +07:00
Nicholas Dudfield
e4875e5398 refactor: remove ttEXPORT_SIGN and UVTx infrastructure
- Delete ExportSign.cpp/h transactor (ttEXPORT_SIGN no longer used)
- Remove isUVTx() function and all UVTx checks from Transactor/TxQ
- Remove ttEXPORT_SIGN from TxFormats enum and format definition
- Remove jss::ExportSign
- Move signPendingExports() to ExportSignatureCollector

Export signatures are now collected ephemerally via TMValidation
messages, not via ttEXPORT_SIGN transactions.
2026-01-28 10:59:45 +07:00
Nicholas Dudfield
5b1b142be0 chore: remove stray iostream includes 2026-01-28 10:34:11 +07:00
Nicholas Dudfield
5ba832204a test: remove unused scaffolding from Export_test
- Remove accept_wasm and emit_wasm hooks (not export-related)
- Remove testBasicSetup, testEmitPayment, testXportPayment
- Keep only testXportPaymentWithValidator which tests the export flow
2026-01-28 10:31:49 +07:00
Nicholas Dudfield
1257b3a65c Merge remote-tracking branch 'origin/dev' into export-uvtxn 2026-01-28 10:21:37 +07:00
Nicholas Dudfield
6013ed2cb6 refactor: remove vestigial on-ledger export signature code
- Remove makeExportSignTxns() function (signatures now via TMValidation)
- Simplify ExportSign::doApply() to no-op (ttEXPORT_SIGN kept for protocol)
- Remove sfSigners from ltEXPORTED_TXN format (collected in memory now)
- Remove unused OpenView include and forward declaration
- Remove vestigial comment in TxQ about makeExportSignTxns
2026-01-28 10:19:14 +07:00
Nicholas Dudfield
034010716e feat: add signature verification cache for export signatures
Add cryptographic verification of export signatures as they arrive:
- stashTxnData() caches serialized txn for verification
- verifyAndAddSignature() verifies against cached data, rejects invalid
- isSignatureVerified() / verifySignature() for Transactor fallback
- Cleanup methods updated to clear verification cache

Also removes leftover debug std::cerr from OpenView, STObject, and tests.
2026-01-27 18:03:55 +07:00
Nicholas Dudfield
b28793b0fa chore: clean up export debug logging
- remove DBG_EXPORT macros and all usages
- remove [EXPORT-TRACE] and [EXPORT-TIMING] debug prefixes
- adjust log levels (verbose logs to trace, summaries to debug)
- upgrade "quorum reached" to info level (important event)
- standardize log prefixes to use "Export:"
- re-enable relay loop in OpenLedger.cpp
- remove reentrant call detection debug code
2026-01-27 16:21:02 +07:00
Nicholas Dudfield
4bce392c31 feat: continuous signature broadcasting for export robustness
Validators now sign ALL pending ltEXPORTED_TXN entries every ledger
(not just those from the current ledger). Signatures are cached in
ExportSignatureCollector and re-broadcast until the export is finalized.

Changes:
- Add hasSignatureFrom() and getSignatureFrom() to collector for
  checking/retrieving cached signatures
- signPendingExports() now iterates ALL pending exports, uses cached
  signature if available, otherwise signs fresh
- Signatures keep broadcasting until ltEXPORTED_TXN is deleted

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)

The ltEXPORTED_TXN acts as a "ticket" - signatures only valid while it
exists. No explicit expiry check needed; ledger state is the gatekeeper.
2026-01-26 18:25:24 +07:00
Nicholas Dudfield
244a28b981 feat: implement ephemeral export signature collection
Replace on-ledger ttEXPORT_SIGN transactions with ephemeral signature
collection via TMValidation messages. This eliminates O(n²) metadata
bloat from accumulating signatures on-ledger.

Changes:
- Add ExportSignatureCollector for in-memory signature storage with
  quorum tracking (80% UNL threshold)
- Extend TMValidation protobuf with exportSignatures field
- Sign pending exports during validate() and broadcast via validation
- Extract signatures from received TMValidation in PeerImp
- TxQ checks quorum from memory instead of ledger
- Inject ttEXPORT when quorum reached (can be ledger N+1 or N+2)
- Clean up collector after ttEXPORT processed

Includes [EXPORT-TIMING] debug logging for timing analysis.
2026-01-26 17:54:17 +07:00
Nicholas Dudfield
f2838351c9 chore: add [EXPORT-TRACE] debug logging for export flow tracing
adds step-by-step trace logging with [EXPORT-TRACE] prefix to track
the complete export transaction lifecycle:
- STEP-1: xport() creates ltEXPORTED_TXN
- STEP-2a: rawTxInsert ttEXPORT_SIGN in callback
- STEP-2b: doApply ttEXPORT_SIGN
- STEP-3a: rawTxInsert ttEXPORT
- STEP-4: doApply ttEXPORT (cleanup)

filter with: grep '\[EXPORT-TRACE\]'
2026-01-23 08:10:33 +07:00
Nicholas Dudfield
dae082d6a5 chore: format files with clang-format 2026-01-22 16:42:05 +07:00
Nicholas Dudfield
619a4a68f7 fix: resolve export feature bugs and add comprehensive tests
- fix Guard.h: add import_whitelist_2 to signature lookup chain
  (was causing "Function type is inconsistent" errors for xport APIs)
- fix InvariantCheck.cpp: add ltEXPORTED_TXN to valid ledger entry types
  (was causing "invalid ledger entry type added" invariant failures)
- add SetHook.cpp: TODO comment documenting API version confusion

- add Export_test.cpp: comprehensive test suite for export feature
  - testBasicSetup: verify hook installation works
  - testEmitPayment: verify emit() flow works
  - testXportPayment: verify xport() creates ltEXPORTED_TXN
  - includes DebugLogs helper for per-partition log levels
  - parameterized runXportTest helper for future validator tests

Note: validator signing flow (ttEXPORT_SIGN) still needs debugging -
causes internal error on env.close() when validator config enabled.
2026-01-22 09:51:50 +07:00
Nicholas Dudfield
4a6db8bb05 Merge remote-tracking branch 'origin/dev' into export-uvtxn 2026-01-22 08:07:58 +07:00
Nicholas Dudfield
c86479bc58 fix: correct xport api signature and sfExportedTxn type usage
- Fix xport hook API whitelist to declare 4 args (I32, I32, I32, I32)
  instead of 2, matching the actual implementation signature
- Fix TxQ.cpp to use emplace_back with STObject for sfExportedTxn
  instead of setFieldVL, since sfExportedTxn is OBJECT type not VL.
  The previous code would throw "Wrong field type" at runtime.
2026-01-22 07:41:12 +07:00
Nicholas Dudfield
dc6a2dc6ff refactor: separate ExportSign transactor from Change
Move ttEXPORT_SIGN handling to dedicated ExportSign transactor class,
following the same pattern as ttENTROPY/Entropy from the RNG feature.
UVTxns (signed validator transactions) should not be mixed with
pseudo-transactions in the Change transactor.

- Create ExportSign.h/cpp with preflight, preclaim, doApply
- Route ttEXPORT_SIGN through ExportSign in applySteps.cpp
- Remove UVTx branches from Change transactor
- Add documentation markers to View.h for inUNLReport functions
2026-01-22 07:41:12 +07:00
Nicholas Dudfield
c01b9a657b feat: implement uvtxn pattern for ttEXPORT_SIGN
Port the UNL Validator Transaction (UVTxn) pattern from the RNG feature
to allow validators to submit signed ttEXPORT_SIGN transactions without
requiring a funded account.

Changes:
- Add isUVTx() to identify UVTxn transaction types
- Add inUNLReport() templates to check validator UNLReport membership
- Add getValidationSecretKey() to Application for signing
- Modify Transactor for UVTxn bypasses (fee, seq, signature checks)
- Add makeExportSignTxns() to generate validator signatures
- Hook into RCLConsensus to submit ttEXPORT_SIGN during accept
- Update applySteps.cpp routing for ttEXPORT_SIGN
- Remove direct ttEXPORT_SIGN injection from TxQ::accept

Note: Currently uses Change transactor with UVTx branches.
May refactor to dedicated ExportSign transactor class.
2026-01-20 13:44:38 +07:00
Nicholas Dudfield
652b181b5d chore: clang format 2026-01-20 12:44:14 +07:00
RichardAH
8329d78f32 Update src/ripple/app/tx/impl/Import.cpp
Co-authored-by: tequ <git@tequ.dev>
2025-12-21 13:42:46 +10:00
RichardAH
bf4579c1d1 Update src/ripple/app/tx/impl/Change.cpp
Co-authored-by: tequ <git@tequ.dev>
2025-12-21 13:42:37 +10:00
RichardAH
73e099eb23 Update src/ripple/app/hook/impl/applyHook.cpp
Co-authored-by: tequ <git@tequ.dev>
2025-12-21 13:42:29 +10:00
RichardAH
2e311b4259 Update src/ripple/app/hook/applyHook.h
Co-authored-by: tequ <git@tequ.dev>
2025-12-21 13:42:20 +10:00
RichardAH
7c8e940091 Merge branch 'dev' into export 2025-12-19 13:27:02 +10:00
Richard Holland
9b90c50789 featureExport compiling, untested 2025-12-19 14:19:17 +11:00
Richard Holland
a18e2cb2c6 remainder of the export feature... untested uncompiled 2025-12-14 19:04:37 +11:00
Richard Holland
be5f425122 change symbol name to xport 2025-12-14 13:27:44 +11:00
Richard Holland
fc6f4762da export hook apis, untested 2025-12-13 15:46:08 +11:00
48 changed files with 3072 additions and 881 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,7 @@
#include <ripple/app/ledger/LocalTxs.h>
#include <ripple/app/ledger/OpenLedger.h>
#include <ripple/app/misc/AmendmentTable.h>
#include <ripple/app/misc/ExportSignatureCollector.h>
#include <ripple/app/misc/HashRouter.h>
#include <ripple/app/misc/LoadFeeTrack.h>
#include <ripple/app/misc/NegativeUNLVote.h>
@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,7 @@
#include <ripple/app/main/Tuning.h>
#include <ripple/app/misc/AmendmentTable.h>
#include <ripple/app/misc/DatagramMonitor.h>
#include <ripple/app/misc/ExportSignatureCollector.h>
#include <ripple/app/misc/HashRouter.h>
#include <ripple/app/misc/LoadFeeTrack.h>
#include <ripple/app/misc/NetworkOPs.h>
@@ -218,6 +219,7 @@ public:
std::unique_ptr<AmendmentTable> m_amendmentTable;
std::unique_ptr<LoadFeeTrack> mFeeTrack;
std::unique_ptr<HashRouter> hashRouter_;
std::unique_ptr<ExportSignatureCollector> exportSignatureCollector_;
RCLValidations mValidations;
std::unique_ptr<LoadManager> m_loadManager;
std::unique_ptr<TxQ> txQ_;
@@ -462,6 +464,9 @@ public:
stopwatch(),
HashRouter::getDefaultHoldTime()))
, exportSignatureCollector_(std::make_unique<ExportSignatureCollector>(
logs_->journal("ExportSignatureCollector")))
, mValidations(
ValidationParms(),
stopwatch(),
@@ -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
{

View File

@@ -66,6 +66,7 @@ using SLE = STLedgerEntry;
using CachedSLEs = TaggedCache<uint256, SLE const>;
class CollectorManager;
class ExportSignatureCollector;
class Family;
class HashRouter;
class Logs;
@@ -184,6 +185,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&

View 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

View File

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

View File

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

View 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

View File

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

View File

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

View File

@@ -74,6 +74,9 @@ private:
TER
applyEmitFailure();
TER
applyExport();
TER
applyUNLReport();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@
#include <ripple/app/ledger/InboundTransactions.h>
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/app/ledger/TransactionMaster.h>
#include <ripple/app/misc/ExportSignatureCollector.h>
#include <ripple/app/misc/HashRouter.h>
#include <ripple/app/misc/LoadFeeTrack.h>
#include <ripple/app/misc/NetworkOPs.h>
@@ -3194,6 +3195,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
{

View File

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

View File

@@ -282,6 +282,11 @@ message TMValidation
// Number of hops traveled
optional uint32 hops = 3 [deprecated = true];
// Export signatures for pending exports validated in this ledger.
// Each entry is: txnHash (32 bytes) + serialized sfSigner STObject.
// Used for ephemeral export signature collection via validation gossip.
repeated bytes exportSignatures = 4;
}
// An array of Endpoint messages

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,6 +67,7 @@ enum TELcodes : TERUnderlyingType {
telNON_LOCAL_EMITTED_TXN,
telIMPORT_VL_KEY_NOT_RECOGNISED,
telCAN_NOT_QUEUE_IMPORT,
telSHADOW_TICKET_REQUIRED,
};
//------------------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

@@ -73,6 +73,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfHookExportCount, soeREQUIRED},
{sfFlags, soeOPTIONAL}});
add(sfHookEmission.jsonName.c_str(),

View File

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

View File

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

View File

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

View File

@@ -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."),

View File

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

View File

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

View File

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

View 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

View 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

View File

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