diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 9728b1040..90cdfc02f 100644 --- a/src/ripple/app/hook/Enum.h +++ b/src/ripple/app/hook/Enum.h @@ -350,7 +350,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 { @@ -364,6 +367,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 uint6_t max_export = 4; const uint8_t max_params = 16; const double fee_base_multiplier = 1.1f; @@ -469,6 +473,13 @@ static const APIWhitelist import_whitelist_1{ // clang-format on }; +static const APIWhitelist import_whitelist_2{ + // clang-format off + HOOK_API_DEFINITION(I64, export, (I32, I32)), + HOOK_API_DEFINITION(I64, export_reserve, (I32)), + // clang-format on +} + #undef HOOK_API_DEFINITION #undef I32 #undef I64 diff --git a/src/ripple/app/hook/Guard.h b/src/ripple/app/hook/Guard.h index f395af448..166d821db 100644 --- a/src/ripple/app/hook/Guard.h +++ b/src/ripple/app/hook/Guard.h @@ -1034,6 +1034,12 @@ validateGuards( { // PASS, this is a version 1 api } + else if (rulesVersion & 0x04U && + hook_api::import_whitelist_2.find(import_name) != + hook_api::import_whitelist_2.end()) + { + // PASS, this is an export api + } else { GUARDLOG(hook::log::IMPORT_ILLEGAL) diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 2c1f250bb..a7bd0a03f 100644 --- a/src/ripple/app/hook/applyHook.h +++ b/src/ripple/app/hook/applyHook.h @@ -406,6 +406,11 @@ DECLARE_HOOK_FUNCTION( uint32_t slot_no_tx, uint32_t slot_no_meta); +DECLARE_HOOK_FUNCTION( + int64_t, + export, + uint32_t read_ptr, + uint32_t read_len); /* DECLARE_HOOK_FUNCTION(int64_t, str_find, uint32_t hread_ptr, uint32_t hread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t mode, @@ -485,6 +490,8 @@ struct HookResult std::queue> emittedTxn{}; // etx stored here until accept/rollback + std::queue> + exportedTxn{}; HookStateMap& stateMap; uint16_t changedStateCount = 0; std::map< @@ -541,6 +548,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 nonce_used{}; uint32_t generation = 0; // used for caching, only generated when txn_generation is called @@ -877,6 +885,9 @@ public: ADD_HOOK_FUNCTION(meta_slot, ctx); ADD_HOOK_FUNCTION(xpop_slot, ctx); + ADD_HOOK_FUNCTION(export, ctx); + ADD_HOOK_FUNCTION(export_reserve, ctx); + /* ADD_HOOK_FUNCTION(str_find, ctx); ADD_HOOK_FUNCTION(str_replace, ctx); diff --git a/src/ripple/app/hook/guard_checker.cpp b/src/ripple/app/hook/guard_checker.cpp index f20d24617..5d1ddf6aa 100644 --- a/src/ripple/app/hook/guard_checker.cpp +++ b/src/ripple/app/hook/guard_checker.cpp @@ -79,7 +79,7 @@ main(int argc, char** argv) close(fd); - auto result = validateGuards(hook, std::cout, "", 3); + auto result = validateGuards(hook, std::cout, "", 7); if (!result) { diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index f27b7d23b..432d369bf 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -1971,6 +1971,8 @@ hook::finalizeHookResult( // directory) if we are allowed to std::vector> emission_txnid; + std::vector + exported_txnid; if (doEmit) { @@ -2026,6 +2028,58 @@ 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 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(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; + applyCtx.view().insert(sleExported); + } + 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); @@ -2052,6 +2106,12 @@ 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); @@ -3888,6 +3948,27 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count) HOOK_TEARDOWN(); } +DEFINE_HOOK_FUNCTION(int64_t, export_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_FUNCNARG(int64_t, etxn_burden) { @@ -6156,6 +6237,92 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } + +DEFINE_HOOK_FUNCTION( + int64_t, + export, + uint32_t read_ptr, + uint32_t read_len, + uint32_t write_ptr, + uint32_t write_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 stpTrans; + try + { + stpTrans = std::make_shared( + 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(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(); +} /* DEFINE_HOOK_FUNCTION( diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 37a436fee..55d1e9155 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -606,7 +606,8 @@ Change::activateXahauGenesis() loggerStream, "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", (ctx_.view().rules().enabled(featureHooksUpdate1) ? 1 : 0) + - (ctx_.view().rules().enabled(fix20250131) ? 2 : 0)); + (ctx_.view().rules().enabled(fix20250131) ? 2 : 0) + + (ctx_.view().rules().enabled(featureExport) ? 4 : 0)); if (!result) { diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp index 9ca829557..a3dddf359 100644 --- a/src/ripple/app/tx/impl/SetHook.cpp +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -491,7 +491,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) logger, hsacc, (ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) + - (ctx.rules.enabled(fix20250131) ? 2 : 0)); + (ctx.rules.enabled(fix20250131) ? 2 : 0) + + (ctx.rules.enabled(featureExport) ? 4 : 0)); if (ctx.j.trace()) { diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 2766859dc..180cd4cc2 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -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 = 90; +static constexpr std::size_t numFeatures = 91; /** 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; } // namespace ripple #endif diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 03f9dc2a4..b21f8adf4 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -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; diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index f87f7ae3f..de5be4357 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -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, diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index f507b2232..9bdfb4e90 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -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; diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 24383f896..de47c7406 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -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); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index f75faf165..38048f614 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -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 { diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index c4f2ef85a..492ff6f09 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -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);