From 663ed4edb84b53a5e9504df13ca6485b4634d0a6 Mon Sep 17 00:00:00 2001 From: tequ Date: Tue, 19 May 2026 11:00:25 +0900 Subject: [PATCH] Named Hook (#718) --- hook/sfcodes.h | 1 + include/xrpl/protocol/detail/features.macro | 1 + include/xrpl/protocol/detail/sfields.macro | 1 + include/xrpl/protocol/jss.h | 1 + src/libxrpl/protocol/InnerObjectFormats.cpp | 1 + src/libxrpl/protocol/TxFormats.cpp | 1 + src/test/app/SetHook_test.cpp | 491 +++++++++++++++++++- src/test/app/SetHook_wasm.h | 187 ++++++++ src/xrpld/app/tx/detail/SetHook.cpp | 72 ++- src/xrpld/app/tx/detail/SetHook.h | 3 + src/xrpld/app/tx/detail/Transactor.cpp | 45 ++ 11 files changed, 792 insertions(+), 12 deletions(-) diff --git a/hook/sfcodes.h b/hook/sfcodes.h index 026d38031..3dc2a4435 100644 --- a/hook/sfcodes.h +++ b/hook/sfcodes.h @@ -221,6 +221,7 @@ #define sfProvider ((7U << 16U) + 30U) #define sfMPTokenMetadata ((7U << 16U) + 31U) #define sfCredentialType ((7U << 16U) + 32U) +#define sfHookName ((7U << 16U) + 97U) #define sfRemarkValue ((7U << 16U) + 98U) #define sfRemarkName ((7U << 16U) + 99U) #define sfAccount ((8U << 16U) + 1U) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index d71aa66db..03303ca93 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -34,6 +34,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(IOURewardClaim, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (IOULockedBalanceInvariant, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (ImportIssuer, Supported::yes, VoteBehavior::DefaultYes) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 4532d0aa6..dcc3138e9 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -293,6 +293,7 @@ TYPED_SFIELD(sfAssetClass, VL, 29) TYPED_SFIELD(sfProvider, VL, 30) TYPED_SFIELD(sfMPTokenMetadata, VL, 31) TYPED_SFIELD(sfCredentialType, VL, 32) +TYPED_SFIELD(sfHookName, VL, 97) TYPED_SFIELD(sfRemarkValue, VL, 98) TYPED_SFIELD(sfRemarkName, VL, 99) diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 964457536..4355544bc 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -76,6 +76,7 @@ JSS(Holder); // field. JSS(HookApiVersion); // field JSS(HookCanEmit); // field JSS(HookHash); // field +JSS(HookName); // field JSS(HookNamespace); // field JSS(HookOn); // field JSS(HookOnIncoming); // field diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 050047a6a..c1853427c 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -99,6 +99,7 @@ InnerObjectFormats::InnerObjectFormats() {sfHookOnOutgoing, soeOPTIONAL}, {sfHookCanEmit, soeOPTIONAL}, {sfHookApiVersion, soeOPTIONAL}, + {sfHookName, soeOPTIONAL}, {sfFlags, soeOPTIONAL}}); add(sfHookGrant.jsonName, diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 777e5e645..55de0ab4d 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -48,6 +48,7 @@ TxFormats::TxFormats() {sfFirstLedgerSequence, soeOPTIONAL}, {sfNetworkID, soeOPTIONAL}, {sfHookParameters, soeOPTIONAL}, + {sfHookName, soeOPTIONAL}, }; #pragma push_macro("UNWRAP") diff --git a/src/test/app/SetHook_test.cpp b/src/test/app/SetHook_test.cpp index b36d18989..f03b7f052 100644 --- a/src/test/app/SetHook_test.cpp +++ b/src/test/app/SetHook_test.cpp @@ -690,6 +690,8 @@ public: bool const hasHookCanEmit = env.current()->rules().enabled(featureHookCanEmit); + bool const hasNamedHooks = + env.current()->rules().enabled(featureNamedHooks); auto const alice = Account{"alice"}; env.fund(XRP(10000), alice); @@ -726,7 +728,8 @@ public: } // grants, parameters, hookon, hookonincoming, hookonoutgoing, - // hookcanemit, hookapiversion, hooknamespace keys must be absent + // hookcanemit, hookapiversion, hooknamespace, hookname keys must be + // absent for (auto const& [key, value] : JSSMap{ {jss::HookGrants, Json::arrayValue}, {jss::HookParameters, Json::arrayValue}, @@ -743,11 +746,16 @@ public: "000000000000000000000000000000000000000000000000000000000000" "0000"}, {jss::HookApiVersion, "0"}, - {jss::HookNamespace, to_string(uint256{beast::zero})}}) + {jss::HookNamespace, to_string(uint256{beast::zero})}, + {jss::HookName, strHex(std::string{"DEADBEEF"})}, + }) { if (!hasHookCanEmit && key == jss::HookCanEmit) continue; + if (!hasNamedHooks && key == jss::HookName) + continue; + Json::Value iv; iv[jss::CreateCode] = ""; iv[key] = value; @@ -755,7 +763,7 @@ public: env(jv, M("Hook DELETE operation cannot include: grants, params, " "hookon, HookOnIncoming, HookOnOutgoing, hookcanemit, " - "apiversion, namespace"), + "apiversion, namespace, hookname"), HSFEE, ter(temMALFORMED)); env.close(); @@ -894,6 +902,8 @@ public: bool const fixNS = env.current()->rules().enabled(fixNSDelete); bool const hasHookCanEmit = env.current()->rules().enabled(featureHookCanEmit); + bool const hasNamedHooks = + env.current()->rules().enabled(featureNamedHooks); auto const alice = Account{"alice"}; env.fund(XRP(10000), alice); @@ -926,11 +936,15 @@ public: "000000000000000000000000000000000000000000000000000000000000" "0000"}, {jss::HookApiVersion, "0"}, + {jss::HookName, strHex(std::string{"DEADBEEF"})}, }) { if (!hasHookCanEmit && key == jss::HookCanEmit) continue; + if (!hasNamedHooks && key == jss::HookName) + continue; + Json::Value iv; iv[key] = value; iv[jss::Flags] = hsfNSDELETE; @@ -939,7 +953,7 @@ public: env(jv, M("Hook NSDELETE operation cannot include: grants, params, " "hookon, hookonincoming, hookonoutgoing, hookcanemit, " - "apiversion"), + "apiversion, hookname"), HSFEE, ter(temMALFORMED)); env.close(); @@ -1684,6 +1698,340 @@ public: } } + void + testHookName(FeatureBitset features) + { + testcase("Test hook name"); + using namespace jtx; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const charlie = Account{"charlie"}; + auto const USD = alice["USD"]; + + Env env{*this, features}; + + env.fund(XRP(10000), alice, bob); + env.close(); + + // Invalid hook name (length=3,17, not utf-8) + for (auto const name : { + "414243", // ABC (length=3) + "4142434445464748494A4B4C4D4E4F5051", // ABCDEFGHIJKLMNOPQ + // (length=17) + "DEADBEEF", // not utf-8 + }) + { + auto jvh = hso(accept_wasm); + jvh[jss::HookName] = name; + env(ripple::test::jtx::hook(alice, {{jvh}}, 0), + M("Hook name must be between 8 and 32 hex characters and be a " + "valid UTF-8 string"), + HSFEE, + features[featureNamedHooks] ? ter(temMALFORMED) + : ter(temDISABLED)); + + auto jvi = invoke::invoke(alice); + jvi[jss::HookName] = name; + env(jvi, + M("Call named hook with the invalid hook name"), + HSFEE, + ter(temMALFORMED)); + } + + if (!features[featureNamedHooks]) + return; + + { + /// Create, Install, Update named Hook + Env env{*this, features}; + + env.fund(XRP(10000), alice, bob, charlie); + env.close(); + + // Create with hook name + auto jvh = hso(accept_wasm); + jvh[jss::HookName] = "41424344"; + env(ripple::test::jtx::hook(alice, {{jvh}}, 0), + M("Create named hook"), + HSFEE); + env.close(); + + // Check hook definition + { + auto const hookDef = + env.le(keylet::hookDefinition(accept_hash)); + BEAST_EXPECT(hookDef); + BEAST_EXPECT(!hookDef->isFieldPresent(sfHookName)); + } + // Check Hook + { + auto const hooks = env.le(keylet::hook(alice)); + BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks)); + auto const& hooksArray = hooks->getFieldArray(sfHooks); + BEAST_EXPECT(hooksArray.size() == 1); + BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName)); + BEAST_EXPECT( + strHex(hooksArray[0].getFieldVL(sfHookName)) == "41424344"); + } + + // install with hook name + jvh[jss::HookName] = "41424344"; + env(ripple::test::jtx::hook(bob, {{jvh}}, 0), + M("Install named hook"), + HSFEE); + env.close(); + // Check Hook + { + auto const hooks = env.le(keylet::hook(bob)); + BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks)); + auto const& hooksArray = hooks->getFieldArray(sfHooks); + BEAST_EXPECT(hooksArray.size() == 1); + BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName)); + BEAST_EXPECT( + strHex(hooksArray[0].getFieldVL(sfHookName)) == "41424344"); + } + + // install without hook name + jvh.removeMember(jss::HookName); + env(ripple::test::jtx::hook(charlie, {{jvh}}, 0), + M("Install non-named hook"), + HSFEE); + env.close(); + // Check Hook + { + auto const hooks = env.le(keylet::hook(charlie)); + BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks)); + auto const& hooksArray = hooks->getFieldArray(sfHooks); + BEAST_EXPECT(hooksArray.size() == 1); + BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName)); + } + + // Update named hook to non-named hook + jvh[jss::HookName] = ""; + jvh[jss::Flags] = hsfOVERRIDE; + env(ripple::test::jtx::hook(alice, {{jvh}}, 0), + M("Update named hook to non-named hook"), + HSFEE); + env.close(); + // Check Hook + { + auto const hooks = env.le(keylet::hook(alice)); + BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks)); + auto const& hooksArray = hooks->getFieldArray(sfHooks); + BEAST_EXPECT(hooksArray.size() == 1); + BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName)); + } + + // Update named hook to named hook + jvh[jss::HookName] = "4142434445"; + jvh[jss::Flags] = hsfOVERRIDE; + env(ripple::test::jtx::hook(bob, {{jvh}}, 0), + M("Update non-named hook to named hook"), + HSFEE); + env.close(); + // Check Hook + { + auto const hooks = env.le(keylet::hook(bob)); + BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks)); + auto const& hooksArray = hooks->getFieldArray(sfHooks); + BEAST_EXPECT(hooksArray.size() == 1); + BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName)); + BEAST_EXPECT( + strHex(hooksArray[0].getFieldVL(sfHookName)) == + "4142434445"); + } + + // Update non-named hook to named hook + jvh[jss::HookName] = "41424344"; + jvh[jss::Flags] = hsfOVERRIDE; + env(ripple::test::jtx::hook(charlie, {{jvh}}, 0), + M("Update non-named hook to named hook"), + HSFEE); + env.close(); + // Check Hook + { + auto const hooks = env.le(keylet::hook(charlie)); + BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks)); + auto const& hooksArray = hooks->getFieldArray(sfHooks); + BEAST_EXPECT(hooksArray.size() == 1); + BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName)); + BEAST_EXPECT( + strHex(hooksArray[0].getFieldVL(sfHookName)) == "41424344"); + } + } + + // Install named hook + auto jvh = hso(accept_wasm); + jvh[jss::HookName] = "41424344"; + jvh[jss::Flags] = hsfCOLLECT; + auto jvh2 = hso(accept2_wasm); + jvh2[jss::Flags] = hsfCOLLECT; + env(ripple::test::jtx::hook(alice, {{jvh, jvh2}}, 0), + M("Install named hook"), + HSFEE); + env.close(); + + // + // Test Strong + // + // Call named hook without specifying the hook name + { + auto jv = invoke::invoke(alice); + auto expectedFee = + calculateBaseFee(*env.current(), *env.jt(jv).stx); + BEAST_EXPECT(expectedFee == drops(19)); + env(jv, + M("Call named hook without specifying the hook name"), + HSFEE); + env.close(); + // execute only non-named hook + auto const hookExecutions = + env.meta()->getFieldArray(sfHookExecutions); + BEAST_EXPECT(hookExecutions.size() == 1); + BEAST_EXPECT( + hookExecutions[0].getFieldH256(sfHookHash) == accept2_hash); + } + + // Call named hook with the wrong hook name + { + auto jv = invoke::invoke(alice); + jv[jss::HookName] = "41424345"; + auto expectedFee = + calculateBaseFee(*env.current(), *env.jt(jv).stx); + BEAST_EXPECT(expectedFee == drops(19)); + env(jv, M("Call named hook with the wrong hook name"), HSFEE); + env.close(); + // execute only non-named hook + auto const hookExecutions = + env.meta()->getFieldArray(sfHookExecutions); + BEAST_EXPECT(hookExecutions.size() == 1); + BEAST_EXPECT( + hookExecutions[0].getFieldH256(sfHookHash) == accept2_hash); + } + + // Call named hook with the correct hook name + { + auto jv = invoke::invoke(alice); + jv[jss::HookName] = "41424344"; + auto expectedFee = + calculateBaseFee(*env.current(), *env.jt(jv).stx); + BEAST_EXPECT(expectedFee == drops(28)); + env(jv, M("Call named hook with the correct hook name"), HSFEE); + env.close(); + // execute both named and non-named hooks + auto const hookExecutions = + env.meta()->getFieldArray(sfHookExecutions); + BEAST_EXPECT(hookExecutions.size() == 2); + BEAST_EXPECT( + hookExecutions[0].getFieldH256(sfHookHash) == accept_hash); + BEAST_EXPECT( + hookExecutions[1].getFieldH256(sfHookHash) == accept2_hash); + } + + // + // Test Weak + // + env(fset(alice, asfTshCollect), fee(XRP(1))); + env.close(); + // Call named hook without specifying the hook name + { + auto jv = trust(bob, USD(1000)); + env(jv, + M("Call named hook without specifying the hook name"), + HSFEE); + env.close(); + // execute only non-named hook + auto const hookExecutions = + env.meta()->getFieldArray(sfHookExecutions); + BEAST_EXPECT(hookExecutions.size() == 1); + BEAST_EXPECT( + hookExecutions[0].getFieldH256(sfHookHash) == accept2_hash); + } + + // Call named hook with the wrong hook name + { + auto jv = trust(bob, USD(1000)); + jv[jss::HookName] = "41424345"; + env(jv, M("Call named hook with the wrong hook name"), HSFEE); + env.close(); + // execute only non-named hook + auto const hookExecutions = + env.meta()->getFieldArray(sfHookExecutions); + BEAST_EXPECT(hookExecutions.size() == 1); + BEAST_EXPECT( + hookExecutions[0].getFieldH256(sfHookHash) == accept2_hash); + } + + // Call named hook with the correct hook name + { + auto jv = invoke::invoke(alice); + jv[jss::HookName] = "41424344"; + env(jv, M("Call named hook with the correct hook name"), HSFEE); + env.close(); + // execute both named and non-named hooks + auto const hookExecutions = + env.meta()->getFieldArray(sfHookExecutions); + BEAST_EXPECT(hookExecutions.size() == 2); + BEAST_EXPECT( + hookExecutions[0].getFieldH256(sfHookHash) == accept_hash); + BEAST_EXPECT( + hookExecutions[1].getFieldH256(sfHookHash) == accept2_hash); + } + env(fclear(alice, asfTshCollect), fee(XRP(1))); + env.close(); + + // + // Callback Execution + // + jvh = hso(emit_invoke_wasm); + jvh[jss::HookName] = "41424344"; + jvh[jss::Flags] = hsfOVERRIDE; + env(ripple::test::jtx::hook(alice, {{jvh}}, 0), + M("Install named callback hook"), + HSFEE); + env.close(); + // Call named hook without specifying the hook name + { + auto jv = invoke::invoke(alice); + env(jv, + M("Call named hook without specifying the hook name"), + HSFEE); + env.close(); + // execute only non-named hook + BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions)); + } + + // Call named hook with the wrong hook name + { + auto jv = invoke::invoke(alice); + jv[jss::HookName] = "41424345"; + env(jv, M("Call named hook with the wrong hook name"), HSFEE); + env.close(); + // execute only non-named hook + BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions)); + } + + // Call named hook with the correct hook name + { + auto jv = invoke::invoke(alice); + jv[jss::HookName] = "41424344"; + env(jv, M("Call named hook with the correct hook name"), HSFEE); + env.close(); + // execute both named and non-named hooks + BEAST_EXPECT(env.meta()->isFieldPresent(sfHookEmissions)); + auto const hookEmissions = + env.meta()->getFieldArray(sfHookEmissions)[0]; + auto const etxn = hookEmissions.getFieldH256(sfEmittedTxnID); + env.close(); + auto const tx = env.closed()->txRead(etxn); + BEAST_EXPECT(tx.first && tx.second); + BEAST_EXPECT(!tx.first->isFieldPresent(sfHookName)); + // Callback transaction doesn't need to have hook name + BEAST_EXPECT(tx.second->isFieldPresent(sfHookExecutions)); + } + } + void testFillCopy(FeatureBitset features) { @@ -1741,6 +2089,8 @@ public: bool const hasHookCanEmit = env.current()->rules().enabled(featureHookCanEmit); + bool const hasNamedHooks = + env.current()->rules().enabled(featureNamedHooks); auto const bob = Account{"bob"}; env.fund(XRP(10000), bob); @@ -1789,6 +2139,8 @@ public: iv[jss::HookCanEmit] = "0000000000000000000000000000000000000000000000000000000000" "000000"; + if (hasNamedHooks) + iv[jss::HookName] = strHex(std::string{"DEADBEEF"}); jv[jss::Hooks][0U] = Json::Value{}; jv[jss::Hooks][0U][jss::Hook] = iv; @@ -1811,6 +2163,8 @@ public: iv[jss::HookCanEmit] = "0000000000000000000000000000000000000000000000000000000000" "000000"; + if (hasNamedHooks) + iv[jss::HookName] = strHex(std::string{"DEADBEEF"}); jv[jss::Hooks][0U] = Json::Value{}; jv[jss::Hooks][0U][jss::Hook] = iv; @@ -1834,6 +2188,8 @@ public: iv[jss::HookCanEmit] = "0000000000000000000000000000000000000000000000000000000000" "000000"; + if (hasNamedHooks) + iv[jss::HookName] = strHex(std::string{"DEADBEEF"}); jv[jss::Hooks][0U] = Json::Value{}; jv[jss::Hooks][0U][jss::Hook] = iv; @@ -1854,6 +2210,8 @@ public: iv[jss::HookCanEmit] = "0000000000000000000000000000000000000000000000000000000000" "000000"; + if (hasNamedHooks) + iv[jss::HookName] = strHex(std::string{"DEADBEEF"}); jv[jss::Hooks][0U] = Json::Value{}; jv[jss::Hooks][0U][jss::Hook] = iv; @@ -2002,6 +2360,8 @@ public: bool const hasHookCanEmit = env.current()->rules().enabled(featureHookCanEmit); + bool const hasNamedHooks = + env.current()->rules().enabled(featureNamedHooks); auto const alice = Account{"alice"}; env.fund(XRP(10000), alice); @@ -2028,6 +2388,8 @@ public: iv[jss::HookCanEmit] = "0000000000000000000000000000000000000000000000000000000000" "000000"; + if (hasNamedHooks) + iv[jss::HookName] = strHex(std::string{"DEADBEEF"}); iv[jss::HookParameters] = Json::Value{Json::arrayValue}; iv[jss::HookParameters][0U] = Json::Value{}; iv[jss::HookParameters][0U][jss::HookParameter] = Json::Value{}; @@ -2116,11 +2478,16 @@ public: "CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE" "CAFECAFE"}, {jss::HookParameters, params}, - {jss::HookGrants, grants}}) + {jss::HookGrants, grants}, + {jss::HookName, strHex(std::string{"DEADBEEF"})}, + }) { if (!hasHookCanEmit && key == jss::HookCanEmit) continue; + if (!hasNamedHooks && key == jss::HookName) + continue; + Json::Value iv; iv[key] = value; jv[jss::Hooks][0U] = Json::Value{}; @@ -2192,11 +2559,16 @@ public: {jss::HookCanEmit, "00000000000000000000000000000000000000000000000000000000" "00000000"}, - {jss::HookNamespace, to_string(uint256{beast::zero})}}) + {jss::HookNamespace, to_string(uint256{beast::zero})}, + {jss::HookName, strHex(std::string{"DEADBEEF"})}, + }) { if (key == jss::HookCanEmit && !hasHookCanEmit) continue; + if (!hasNamedHooks && key == jss::HookName) + continue; + Json::Value iv; iv[key] = value; jv[jss::Hooks][0U] = Json::Value{}; @@ -2445,11 +2817,16 @@ public: "CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE" "CAFECAFE"}, {jss::HookParameters, params}, - {jss::HookGrants, grants}}) + {jss::HookGrants, grants}, + {jss::HookName, strHex(std::string{"DEADBEEF"})}, + }) { if (key == jss::HookCanEmit && !hasHookCanEmit) continue; + if (!hasNamedHooks && key == jss::HookName) + continue; + Json::Value iv; iv[key] = value; jv[jss::Hooks][0U] = Json::Value{}; @@ -14721,6 +15098,7 @@ public: testPageCap(features); testHookOnV2(features); + testHookName(features); testFillCopy(features); @@ -14824,7 +15202,7 @@ public: using namespace test::jtx; static FeatureBitset const all{supported_amendments()}; - static std::array const feats{ + static std::array const feats{ all, all - fixXahauV2, all - fixXahauV1 - fixXahauV2, @@ -14834,6 +15212,7 @@ public: featureHookCanEmit, all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap - featureExtendedHookState, + all - featureNamedHooks, }; if (BEAST_EXPECT(instance < feats.size())) @@ -14985,6 +15364,98 @@ private: )[test.hook]"]; HASH_WASM(accept2); + + // This hook is used to test Callback + TestHook emit_invoke_wasm = // WASM: 7 + wasm[ + R"[test.hook]( + #include + 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 emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); + extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len); + extern int64_t etxn_reserve(uint32_t); + extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len); + extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len); + extern int64_t ledger_seq (void); + + #define SBUF(x) (uint32_t)x,sizeof(x) + // clang-format off + uint8_t txn[229] = + { + /* size, upto, field name */ + /* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U, + /* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U, + /* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U, + /* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U, + /* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U, + /* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + /* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + /* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + /* 116, 91, emit details */ + /* 0, 229, */ + }; + // clang-format on + + // TX BUILDER + #define FLAGS_OUT (txn + 4U) + #define FLS_OUT (txn + 15U) + #define LLS_OUT (txn + 21U) + #define FEE_OUT (txn + 26U) + #define ACCOUNT_OUT (txn + 71U) + #define EMIT_OUT (txn + 91U) + + #define FLIP_ENDIAN_32(value) \ + (uint32_t)(((value & 0xFFU) << 24) | ((value & 0xFF00U) << 8) | \ + ((value & 0xFF0000U) >> 8) | ((value & 0xFF000000U) >> 24)) + + #define SET_UINT32(ptr, value) *((uint32_t *)(ptr)) = FLIP_ENDIAN_32(value); + + #define SET_NATIVE_AMOUNT(ptr, amount) \ + do { \ + uint8_t *b = (ptr); \ + *b++ = 0b01000000 + ((amount >> 56) & 0b00111111); \ + *b++ = (amount >> 48) & 0xFFU; \ + *b++ = (amount >> 40) & 0xFFU; \ + *b++ = (amount >> 32) & 0xFFU; \ + *b++ = (amount >> 24) & 0xFFU; \ + *b++ = (amount >> 16) & 0xFFU; \ + *b++ = (amount >> 8) & 0xFFU; \ + *b++ = (amount >> 0) & 0xFFU; \ + } while (0) + + #define PREPARE_TXN() \ + do { \ + etxn_reserve(1); \ + uint32_t fls = (uint32_t)ledger_seq() + 1; \ + SET_UINT32(FLS_OUT, fls); \ + SET_UINT32(LLS_OUT, fls + 4); \ + hook_account(ACCOUNT_OUT, 20); \ + etxn_details(EMIT_OUT, 138U); \ + int64_t fee = etxn_fee_base(SBUF(txn)); \ + SET_NATIVE_AMOUNT(FEE_OUT, fee); \ + } while (0) + + + int64_t cbak(uint32_t r) + { + _g(1,1); + return accept(0,0,0); + } + + int64_t hook(uint32_t reserved) + { + _g(1,1); + PREPARE_TXN(); + uint8_t emithash[32]; + int64_t emit_result = emit(SBUF(emithash), SBUF(txn)); + if (emit_result < 0) + return rollback(SBUF("Emit failed."), __LINE__); + return accept(SBUF("Emit succeeded."), __LINE__); + } + )[test.hook]"]; + + HASH_WASM(emit_invoke); }; #define SETHOOK_TEST(i, last) \ @@ -15002,7 +15473,8 @@ SETHOOK_TEST(2, false) SETHOOK_TEST(3, false) SETHOOK_TEST(4, false) SETHOOK_TEST(5, false) -SETHOOK_TEST(6, true) +SETHOOK_TEST(6, false) +SETHOOK_TEST(7, true) BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2); @@ -15011,6 +15483,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2); +BEAST_DEFINE_TESTSUITE_PRIO(SetHook7, app, ripple, 2); } // namespace test } // namespace ripple #undef M diff --git a/src/test/app/SetHook_wasm.h b/src/test/app/SetHook_wasm.h index b54e41550..ab66aeafe 100644 --- a/src/test/app/SetHook_wasm.h +++ b/src/test/app/SetHook_wasm.h @@ -34110,6 +34110,193 @@ std::map> wasm = { 0x0BU, }}, + /* ==== WASM: 100 ==== */ + {R"[test.hook]( + #include + 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 emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); + extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len); + extern int64_t etxn_reserve(uint32_t); + extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len); + extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len); + extern int64_t ledger_seq (void); + + #define SBUF(x) (uint32_t)x,sizeof(x) + // clang-format off + uint8_t txn[229] = + { + /* size, upto, field name */ + /* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U, + /* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U, + /* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U, + /* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U, + /* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U, + /* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + /* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + /* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + /* 116, 91, emit details */ + /* 0, 229, */ + }; + // clang-format on + + // TX BUILDER + #define FLAGS_OUT (txn + 4U) + #define FLS_OUT (txn + 15U) + #define LLS_OUT (txn + 21U) + #define FEE_OUT (txn + 26U) + #define ACCOUNT_OUT (txn + 71U) + #define EMIT_OUT (txn + 91U) + + #define FLIP_ENDIAN_32(value) \ + (uint32_t)(((value & 0xFFU) << 24) | ((value & 0xFF00U) << 8) | \ + ((value & 0xFF0000U) >> 8) | ((value & 0xFF000000U) >> 24)) + + #define SET_UINT32(ptr, value) *((uint32_t *)(ptr)) = FLIP_ENDIAN_32(value); + + #define SET_NATIVE_AMOUNT(ptr, amount) \ + do { \ + uint8_t *b = (ptr); \ + *b++ = 0b01000000 + ((amount >> 56) & 0b00111111); \ + *b++ = (amount >> 48) & 0xFFU; \ + *b++ = (amount >> 40) & 0xFFU; \ + *b++ = (amount >> 32) & 0xFFU; \ + *b++ = (amount >> 24) & 0xFFU; \ + *b++ = (amount >> 16) & 0xFFU; \ + *b++ = (amount >> 8) & 0xFFU; \ + *b++ = (amount >> 0) & 0xFFU; \ + } while (0) + + #define PREPARE_TXN() \ + do { \ + etxn_reserve(1); \ + uint32_t fls = (uint32_t)ledger_seq() + 1; \ + SET_UINT32(FLS_OUT, fls); \ + SET_UINT32(LLS_OUT, fls + 4); \ + hook_account(ACCOUNT_OUT, 20); \ + etxn_details(EMIT_OUT, 138U); \ + int64_t fee = etxn_fee_base(SBUF(txn)); \ + SET_NATIVE_AMOUNT(FEE_OUT, fee); \ + } while (0) + + + int64_t cbak(uint32_t r) + { + _g(1,1); + return accept(0,0,0); + } + + int64_t hook(uint32_t reserved) + { + _g(1,1); + PREPARE_TXN(); + uint8_t emithash[32]; + int64_t emit_result = emit(SBUF(emithash), SBUF(txn)); + if (emit_result < 0) + return rollback(SBUF("Emit failed."), __LINE__); + return accept(SBUF("Emit succeeded."), __LINE__); + } + )[test.hook]", + { + 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x25U, + 0x06U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU, + 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x60U, + 0x00U, 0x01U, 0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, + 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x8FU, 0x01U, + 0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U, + 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, + 0x74U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U, + 0x78U, 0x6EU, 0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, + 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, + 0x67U, 0x65U, 0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x03U, 0x03U, + 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, + 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x04U, 0x03U, 0x65U, + 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, + 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, 0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, + 0x76U, 0x0DU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x66U, 0x65U, 0x65U, + 0x5FU, 0x62U, 0x61U, 0x73U, 0x65U, 0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, + 0x76U, 0x04U, 0x65U, 0x6DU, 0x69U, 0x74U, 0x00U, 0x05U, 0x03U, 0x65U, + 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, + 0x6BU, 0x00U, 0x01U, 0x03U, 0x03U, 0x02U, 0x02U, 0x02U, 0x05U, 0x03U, + 0x01U, 0x00U, 0x02U, 0x06U, 0x27U, 0x06U, 0x7FU, 0x01U, 0x41U, 0x90U, + 0x8AU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x82U, 0x0AU, 0x0BU, 0x7FU, + 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x90U, 0x8AU, + 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, + 0x41U, 0x80U, 0x08U, 0x0BU, 0x07U, 0x0FU, 0x02U, 0x04U, 0x63U, 0x62U, + 0x61U, 0x6BU, 0x00U, 0x09U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, + 0x0AU, 0x0AU, 0xA8U, 0x83U, 0x00U, 0x02U, 0x99U, 0x80U, 0x00U, 0x00U, + 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, + 0x1AU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U, + 0x80U, 0x80U, 0x00U, 0x0BU, 0x88U, 0x83U, 0x00U, 0x02U, 0x03U, 0x7FU, + 0x01U, 0x7EU, 0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x20U, + 0x6BU, 0x22U, 0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, + 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, + 0x41U, 0x01U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, + 0x00U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0xA7U, 0x22U, 0x02U, + 0x41U, 0x05U, 0x6AU, 0x22U, 0x03U, 0x41U, 0x18U, 0x74U, 0x20U, 0x03U, + 0x41U, 0x08U, 0x74U, 0x41U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x71U, 0x72U, + 0x20U, 0x03U, 0x41U, 0x08U, 0x76U, 0x41U, 0x80U, 0xFEU, 0x03U, 0x71U, + 0x20U, 0x03U, 0x41U, 0x18U, 0x76U, 0x72U, 0x72U, 0x36U, 0x02U, 0x95U, + 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x41U, 0x01U, + 0x6AU, 0x22U, 0x03U, 0x41U, 0x18U, 0x74U, 0x20U, 0x03U, 0x41U, 0x08U, + 0x74U, 0x41U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x71U, 0x72U, 0x20U, 0x03U, + 0x41U, 0x08U, 0x76U, 0x41U, 0x80U, 0xFEU, 0x03U, 0x71U, 0x20U, 0x03U, + 0x41U, 0x18U, 0x76U, 0x72U, 0x72U, 0x36U, 0x02U, 0x8FU, 0x88U, 0x80U, + 0x80U, 0x00U, 0x41U, 0xC7U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x14U, + 0x10U, 0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, 0xDBU, 0x88U, + 0x80U, 0x80U, 0x00U, 0x41U, 0x8AU, 0x01U, 0x10U, 0x85U, 0x80U, 0x80U, + 0x80U, 0x00U, 0x1AU, 0x41U, 0x00U, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U, + 0x00U, 0x41U, 0xE5U, 0x01U, 0x10U, 0x86U, 0x80U, 0x80U, 0x80U, 0x00U, + 0x22U, 0x04U, 0x3CU, 0x00U, 0xA1U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, + 0x00U, 0x20U, 0x04U, 0x42U, 0x08U, 0x88U, 0x3CU, 0x00U, 0xA0U, 0x88U, + 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x04U, 0x42U, 0x10U, 0x88U, + 0x3CU, 0x00U, 0x9FU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, + 0x04U, 0x42U, 0x18U, 0x88U, 0x3CU, 0x00U, 0x9EU, 0x88U, 0x80U, 0x80U, + 0x00U, 0x41U, 0x00U, 0x20U, 0x04U, 0x42U, 0x20U, 0x88U, 0x3CU, 0x00U, + 0x9DU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x04U, 0x42U, + 0x28U, 0x88U, 0x3CU, 0x00U, 0x9CU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, + 0x00U, 0x20U, 0x04U, 0x42U, 0x30U, 0x88U, 0x3CU, 0x00U, 0x9BU, 0x88U, + 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x04U, 0x42U, 0x38U, 0x88U, + 0xA7U, 0x41U, 0x3FU, 0x71U, 0x41U, 0xC0U, 0x00U, 0x72U, 0x3AU, 0x00U, + 0x9AU, 0x88U, 0x80U, 0x80U, 0x00U, 0x02U, 0x40U, 0x02U, 0x40U, 0x20U, + 0x01U, 0x41U, 0x20U, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, + 0xE5U, 0x01U, 0x10U, 0x87U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x7FU, + 0x55U, 0x0DU, 0x00U, 0x41U, 0xE5U, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, + 0x0DU, 0x42U, 0xD3U, 0x00U, 0x10U, 0x88U, 0x80U, 0x80U, 0x80U, 0x00U, + 0x21U, 0x04U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0xF2U, 0x89U, 0x80U, 0x80U, + 0x00U, 0x41U, 0x10U, 0x42U, 0xD4U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, + 0x80U, 0x00U, 0x21U, 0x04U, 0x0BU, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU, + 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x04U, 0x0BU, 0x0BU, + 0x90U, 0x02U, 0x02U, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0xE5U, 0x01U, + 0x12U, 0x00U, 0x63U, 0x22U, 0x00U, 0x00U, 0x00U, 0x00U, 0x24U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U, 0x20U, + 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x73U, 0x21U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x81U, + 0x14U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x41U, 0xE5U, 0x09U, 0x0BU, 0x1DU, 0x45U, 0x6DU, 0x69U, 0x74U, 0x20U, + 0x66U, 0x61U, 0x69U, 0x6CU, 0x65U, 0x64U, 0x2EU, 0x00U, 0x45U, 0x6DU, + 0x69U, 0x74U, 0x20U, 0x73U, 0x75U, 0x63U, 0x63U, 0x65U, 0x65U, 0x64U, + 0x65U, 0x64U, 0x2EU, 0x00U, + }}, + }; } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/SetHook.cpp b/src/xrpld/app/tx/detail/SetHook.cpp index c7f2ffd6a..53b3b6b7e 100644 --- a/src/xrpld/app/tx/detail/SetHook.cpp +++ b/src/xrpld/app/tx/detail/SetHook.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -230,6 +231,7 @@ SetHook::inferOperation(STObject const& hookSetObj) hookSetObj.isFieldPresent(sfHookOnIncoming))) && !hookSetObj.isFieldPresent(sfHookCanEmit) && !hookSetObj.isFieldPresent(sfHookApiVersion) && + !hookSetObj.isFieldPresent(sfHookName) && !hookSetObj.isFieldPresent(sfFlags)) return hsoNOOP; @@ -267,6 +269,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) hookSetObj.isFieldPresent(sfHookOnIncoming) || hookSetObj.isFieldPresent(sfHookCanEmit) || hookSetObj.isFieldPresent(sfHookApiVersion) || + hookSetObj.isFieldPresent(sfHookName) || !hookSetObj.isFieldPresent(sfFlags) || !hookSetObj.isFieldPresent(sfHookNamespace)) { @@ -300,6 +303,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) hookSetObj.isFieldPresent(sfHookCanEmit) || hookSetObj.isFieldPresent(sfHookApiVersion) || hookSetObj.isFieldPresent(sfHookNamespace) || + hookSetObj.isFieldPresent(sfHookName) || !hookSetObj.isFieldPresent(sfFlags)) { JLOG(ctx.j.trace()) @@ -510,6 +514,14 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) // pass } + // validate sfHookName + if (hookSetObj.isFieldPresent(sfHookName)) + { + auto name = hookSetObj.getFieldVL(sfHookName); + if (!validateHookName(name, ctx.j)) + return false; + } + // finally validate web assembly byte code { if (!hookSetObj.isFieldPresent(sfCreateCode)) @@ -609,6 +621,23 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) } } +bool +SetHook::validateHookName(Blob const& name, beast::Journal const& j) +{ + if (name.size() != 0 && (name.size() < 4 || 16 < name.size())) + { + JLOG(j.trace()) + << "sfHookName must be between 8 and 32 hex characters."; + return false; + } + if (!URIToken::validateUTF8(name)) + { + JLOG(j.trace()) << "sfHookName must be a valid UTF-8 string."; + return false; + } + return true; +} + // Note that if fee calculation causes an overflow then INITIAL_XRP is returned // as a way of ensuring that the txn cannot possibly meet the fee requirement. XRPAmount @@ -783,6 +812,10 @@ SetHook::preflight(PreflightContext const& ctx) hookSetObj.isFieldPresent(sfHookCanEmit)) return temDISABLED; + if (!ctx.rules.enabled(featureNamedHooks) && + hookSetObj.isFieldPresent(sfHookName)) + return temDISABLED; + for (auto const& hookSetElement : hookSetObj) { auto const& name = hookSetElement.getFName(); @@ -792,7 +825,7 @@ SetHook::preflight(PreflightContext const& ctx) name != sfHookOn && name != sfHookOnOutgoing && name != sfHookOnIncoming && name != sfHookGrants && name != sfHookApiVersion && name != sfFlags && - name != sfHookCanEmit) + name != sfHookCanEmit && name != sfHookName) { JLOG(ctx.j.trace()) << "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")[" @@ -1348,6 +1381,8 @@ SetHook::setHook() std::optional newHookCanEmit; std::optional defHookCanEmit; + std::optional newHookName; + // when hsoCREATE is invoked it populates this variable in case the hook // definition already exists and the operation falls through into a // hsoINSTALL operation instead @@ -1416,7 +1451,6 @@ SetHook::setHook() if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookCanEmit)) defHookCanEmit = oldDefSLE->getFieldH256(sfHookCanEmit); - if (oldHook && oldHook->get().isFieldPresent(sfHookCanEmit)) oldHookCanEmit = oldHook->get().getFieldH256(sfHookCanEmit); else if (defHookCanEmit) @@ -1453,6 +1487,9 @@ SetHook::setHook() newNamespace = hookSetObj->get().getFieldH256(sfHookNamespace); newDirKeylet = keylet::hookStateDir(account_, *newNamespace); } + + if (hookSetObj->get().isFieldPresent(sfHookName)) + newHookName = hookSetObj->get().getFieldVL(sfHookName); } // users may destroy a namespace in any operation except NOOP and @@ -1574,6 +1611,9 @@ SetHook::setHook() newHook.setFieldH256( sfHookNamespace, oldHook->get().getFieldH256(sfHookNamespace)); + if (oldHook->get().isFieldPresent(sfHookName)) + newHook.setFieldVL( + sfHookName, oldHook->get().getFieldVL(sfHookName)); // set the namespace if it differs from the definition namespace if (newNamespace) @@ -1638,6 +1678,20 @@ SetHook::setHook() newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit); } + // set the hookname field on ltHook when it is explicitly + // provided + if (newHookName) + { + if (newHookName->size() == 0) + { + newHook.makeFieldAbsent(sfHookName); + } + else + { + newHook.setFieldVL(sfHookName, *newHookName); + } + } + // parameters if (hookSetObj->get().isFieldPresent(sfHookParameters) && hookSetObj->get().getFieldArray(sfHookParameters).empty()) @@ -1839,6 +1893,12 @@ SetHook::setHook() newHook.setFieldArray(sfHookGrants, grants); } + if (hookSetObj->get().isFieldPresent(sfHookName) && + hookSetObj->get().getFieldVL(sfHookName).size() > 0) + newHook.setFieldVL( + sfHookName, + hookSetObj->get().getFieldVL(sfHookName)); + slesToInsert.emplace(keylet, newHookDef); newHook.setFieldH256(sfHookHash, *createHookHash); newHooks.push_back(std::move(newHook)); @@ -1950,6 +2010,11 @@ SetHook::setHook() *defHookCanEmit == *newHookCanEmit)) newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit); + // set the hookname field on ltHook when it is explicitly + // provided + if (newHookName && newHookName->size() > 0) + newHook.setFieldVL(sfHookName, *newHookName); + // parameters TER result = updateHookParameters( ctx, @@ -1999,7 +2064,8 @@ SetHook::setHook() // sfParameters: 1 reserve PER entry // sfGrants are: 1 reserve PER entry // sfHookHash, sfHookNamespace, sfHookOn, sfHookOnOutgoing, - // sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags: free + // sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags, + // sfHookName: free // ltHookDefinition is not reserved because it is an unowned object, // rather the uploader is billed via fee according to the following: diff --git a/src/xrpld/app/tx/detail/SetHook.h b/src/xrpld/app/tx/detail/SetHook.h index 8aa53b54c..db67715e4 100644 --- a/src/xrpld/app/tx/detail/SetHook.h +++ b/src/xrpld/app/tx/detail/SetHook.h @@ -91,6 +91,9 @@ public: static HookSetValidation validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj); + static bool + validateHookName(Blob const& name, beast::Journal const& j); + static uint32_t computeHookReserve(STObject const& hookObj); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2437f3185..5ffe29407 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,16 @@ preflight1(PreflightContext const& ctx) } } + if (ctx.tx.isFieldPresent(sfHookName)) + { + if (!ctx.rules.enabled(featureHooks) || + !ctx.rules.enabled(featureNamedHooks)) + return temMALFORMED; + + if (!SetHook::validateHookName(ctx.tx.getFieldVL(sfHookName), ctx.j)) + return temMALFORMED; + } + auto const spk = ctx.tx.getSigningPubKey(); if (!spk.empty() && !publicKeyType(makeSlice(spk))) @@ -266,8 +277,24 @@ Transactor::calculateHookChainFee( // at the same ledger the fee calculation for it can no longer occur if (!hookDef) { + // LCOV_EXCL_START printf("calculateHookChainFee edge case\n"); continue; + // LCOV_EXCL_STOP + } + + std::optional requiredHookName; + if (hookObj.isFieldPresent(sfHookName) && + hookObj.getFieldVL(sfHookName).size() > 0) + requiredHookName = hookObj.getFieldVL(sfHookName); + + if (requiredHookName) + { + // need to specify same hook name in the transaction + if (!tx.isFieldPresent(sfHookName)) + continue; + if (*requiredHookName != tx.getFieldVL(sfHookName)) + continue; } uint32_t flags = 0; @@ -1311,16 +1338,34 @@ Transactor::executeHookChain( if (hookSkips.find(hookHash) != hookSkips.end()) { + // LCOV_EXCL_START JLOG(j_.trace()) << "HookInfo: Skipping " << hookHash; continue; + // LCOV_EXCL_STOP } auto const& hookDef = ctx_.view().peek(keylet::hookDefinition(hookHash)); if (!hookDef) { + // LCOV_EXCL_START JLOG(j_.warn()) << "HookError[]: Failure: hook def missing (send)"; continue; + // LCOV_EXCL_STOP + } + + std::optional requiredHookName; + if (hookObj.isFieldPresent(sfHookName) && + hookObj.getFieldVL(sfHookName).size() > 0) + requiredHookName = hookObj.getFieldVL(sfHookName); + + if (requiredHookName) + { + // need to specify same hook name in the transaction + if (!ctx_.tx.isFieldPresent(sfHookName)) + continue; + if (*requiredHookName != ctx_.tx.getFieldVL(sfHookName)) + continue; } // check if the hook can fire