From 9120fffd954196064b99dba23263438dcfa0ed2c Mon Sep 17 00:00:00 2001 From: tequ Date: Sun, 15 Feb 2026 18:43:41 +0900 Subject: [PATCH 1/5] Hook API Refactor2: Amendment Guards (#621) --- src/ripple/app/hook/Enum.h | 204 +++++++++++++++----------- src/ripple/app/hook/Guard.h | 46 +++--- src/ripple/app/hook/guard_checker.cpp | 12 +- src/ripple/app/tx/impl/Change.cpp | 5 +- src/ripple/app/tx/impl/SetHook.cpp | 4 +- src/test/app/SetHook_test.cpp | 15 +- 6 files changed, 165 insertions(+), 121 deletions(-) diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 9728b1040..0ef817def 100644 --- a/src/ripple/app/hook/Enum.h +++ b/src/ripple/app/hook/Enum.h @@ -5,6 +5,26 @@ #include #ifndef HOOKENUM_INCLUDED #define HOOKENUM_INCLUDED 1 + +#ifndef GUARD_CHECKER_BUILD +#include +#include +#else +// Override Feature and Rules for guard checker build +#define featureHooksUpdate1 1 +#define fix20250131 1 +namespace hook_api { +struct Rules +{ + constexpr bool + enabled(int feature) const + { + return true; + } +}; +} // namespace hook_api +#endif + namespace ripple { enum HookSetOperation : int8_t { hsoINVALID = -1, @@ -372,105 +392,115 @@ const double fee_base_multiplier = 1.1f; #define HOOK_WRAP_PARAMS(...) __VA_ARGS__ #define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE) \ - { \ -#FUNCTION_NAME, \ - { \ - RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE \ - } \ - } + whitelist[#FUNCTION_NAME] = {RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE}; using APIWhitelist = std::map>; // RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and // hookapi.h (include for hooks) this is a map of the api name to its return // code (vec[0] and its parameters vec[>0]) as wasm type codes -static const APIWhitelist import_whitelist{ +inline APIWhitelist +getImportWhitelist(Rules const& rules) +{ + APIWhitelist whitelist; // clang-format off - HOOK_API_DEFINITION(I32, _g, (I32, I32)), - HOOK_API_DEFINITION(I64, accept, (I32, I32, I64)), - HOOK_API_DEFINITION(I64, rollback, (I32, I32, I64)), - HOOK_API_DEFINITION(I64, util_raddr, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, util_accid, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, util_verify, (I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, util_sha512h, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, util_keylet, (I32, I32, I32, I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, sto_validate, (I32, I32)), - HOOK_API_DEFINITION(I64, sto_subfield, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, sto_subarray, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, sto_emplace, (I32, I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, sto_erase, (I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, etxn_burden, ()), - HOOK_API_DEFINITION(I64, etxn_details, (I32, I32)), - HOOK_API_DEFINITION(I64, etxn_fee_base, (I32, I32)), - HOOK_API_DEFINITION(I64, etxn_reserve, (I32)), - HOOK_API_DEFINITION(I64, etxn_generation, ()), - HOOK_API_DEFINITION(I64, etxn_nonce, (I32, I32)), - HOOK_API_DEFINITION(I64, emit, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, float_set, (I32, I64)), - HOOK_API_DEFINITION(I64, float_multiply, (I64, I64)), - HOOK_API_DEFINITION(I64, float_mulratio, (I64, I32, I32, I32)), - HOOK_API_DEFINITION(I64, float_negate, (I64)), - HOOK_API_DEFINITION(I64, float_compare, (I64, I64, I32)), - HOOK_API_DEFINITION(I64, float_sum, (I64, I64)), - HOOK_API_DEFINITION(I64, float_sto, (I32, I32, I32, I32, I32, I32, I64, I32)), - HOOK_API_DEFINITION(I64, float_sto_set, (I32, I32)), - HOOK_API_DEFINITION(I64, float_invert, (I64)), - HOOK_API_DEFINITION(I64, float_divide, (I64, I64)), - HOOK_API_DEFINITION(I64, float_one, ()), - HOOK_API_DEFINITION(I64, float_mantissa, (I64)), - HOOK_API_DEFINITION(I64, float_sign, (I64)), - HOOK_API_DEFINITION(I64, float_int, (I64, I32, I32)), - HOOK_API_DEFINITION(I64, float_log, (I64)), - HOOK_API_DEFINITION(I64, float_root, (I64, I32)), - HOOK_API_DEFINITION(I64, fee_base, ()), - HOOK_API_DEFINITION(I64, ledger_seq, ()), - HOOK_API_DEFINITION(I64, ledger_last_time, ()), - HOOK_API_DEFINITION(I64, ledger_last_hash, (I32, I32)), - HOOK_API_DEFINITION(I64, ledger_nonce, (I32, I32)), - HOOK_API_DEFINITION(I64, ledger_keylet, (I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, hook_account, (I32, I32)), - HOOK_API_DEFINITION(I64, hook_hash, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, hook_param_set, (I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, hook_param, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, hook_again, ()), - HOOK_API_DEFINITION(I64, hook_skip, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, hook_pos, ()), - HOOK_API_DEFINITION(I64, slot, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, slot_clear, (I32)), - HOOK_API_DEFINITION(I64, slot_count, (I32)), - HOOK_API_DEFINITION(I64, slot_set, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, slot_size, (I32)), - HOOK_API_DEFINITION(I64, slot_subarray, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, slot_subfield, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, slot_type, (I32, I32)), - HOOK_API_DEFINITION(I64, slot_float, (I32)), - HOOK_API_DEFINITION(I64, state_set, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, state_foreign_set, (I32, I32, I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, state, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, state_foreign, (I32, I32, I32, I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, trace, (I32, I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, trace_num, (I32, I32, I64)), - HOOK_API_DEFINITION(I64, trace_float, (I32, I32, I64)), - HOOK_API_DEFINITION(I64, otxn_burden, ()), - HOOK_API_DEFINITION(I64, otxn_field, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, otxn_generation, ()), - HOOK_API_DEFINITION(I64, otxn_id, (I32, I32, I32)), - HOOK_API_DEFINITION(I64, otxn_type, ()), - HOOK_API_DEFINITION(I64, otxn_slot, (I32)), - HOOK_API_DEFINITION(I64, otxn_param, (I32, I32, I32, I32)), - HOOK_API_DEFINITION(I64, meta_slot, (I32)), - // clang-format on -}; + HOOK_API_DEFINITION(I32, _g, (I32, I32)) + HOOK_API_DEFINITION(I64, accept, (I32, I32, I64)) + HOOK_API_DEFINITION(I64, rollback, (I32, I32, I64)) + HOOK_API_DEFINITION(I64, util_raddr, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, util_accid, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, util_verify, (I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, util_sha512h, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, util_keylet, (I32, I32, I32, I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, sto_validate, (I32, I32)) + HOOK_API_DEFINITION(I64, sto_subfield, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, sto_subarray, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, sto_emplace, (I32, I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, sto_erase, (I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, etxn_burden, ()) + HOOK_API_DEFINITION(I64, etxn_details, (I32, I32)) + HOOK_API_DEFINITION(I64, etxn_fee_base, (I32, I32)) + HOOK_API_DEFINITION(I64, etxn_reserve, (I32)) + HOOK_API_DEFINITION(I64, etxn_generation, ()) + HOOK_API_DEFINITION(I64, etxn_nonce, (I32, I32)) + HOOK_API_DEFINITION(I64, emit, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, float_set, (I32, I64)) + HOOK_API_DEFINITION(I64, float_multiply, (I64, I64)) + HOOK_API_DEFINITION(I64, float_mulratio, (I64, I32, I32, I32)) + HOOK_API_DEFINITION(I64, float_negate, (I64)) + HOOK_API_DEFINITION(I64, float_compare, (I64, I64, I32)) + HOOK_API_DEFINITION(I64, float_sum, (I64, I64)) + HOOK_API_DEFINITION(I64, float_sto, (I32, I32, I32, I32, I32, I32, I64, I32)) + HOOK_API_DEFINITION(I64, float_sto_set, (I32, I32)) + HOOK_API_DEFINITION(I64, float_invert, (I64)) + HOOK_API_DEFINITION(I64, float_divide, (I64, I64)) + HOOK_API_DEFINITION(I64, float_one, ()) + HOOK_API_DEFINITION(I64, float_mantissa, (I64)) + HOOK_API_DEFINITION(I64, float_sign, (I64)) + HOOK_API_DEFINITION(I64, float_int, (I64, I32, I32)) + HOOK_API_DEFINITION(I64, float_log, (I64)) + HOOK_API_DEFINITION(I64, float_root, (I64, I32)) + HOOK_API_DEFINITION(I64, fee_base, ()) + HOOK_API_DEFINITION(I64, ledger_seq, ()) + HOOK_API_DEFINITION(I64, ledger_last_time, ()) + HOOK_API_DEFINITION(I64, ledger_last_hash, (I32, I32)) + HOOK_API_DEFINITION(I64, ledger_nonce, (I32, I32)) + HOOK_API_DEFINITION(I64, ledger_keylet, (I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, hook_account, (I32, I32)) + HOOK_API_DEFINITION(I64, hook_hash, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, hook_param_set, (I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, hook_param, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, hook_again, ()) + HOOK_API_DEFINITION(I64, hook_skip, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, hook_pos, ()) + HOOK_API_DEFINITION(I64, slot, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, slot_clear, (I32)) + HOOK_API_DEFINITION(I64, slot_count, (I32)) + HOOK_API_DEFINITION(I64, slot_set, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, slot_size, (I32)) + HOOK_API_DEFINITION(I64, slot_subarray, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, slot_subfield, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, slot_type, (I32, I32)) + HOOK_API_DEFINITION(I64, slot_float, (I32)) + HOOK_API_DEFINITION(I64, state_set, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, state_foreign_set, (I32, I32, I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, state, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, state_foreign, (I32, I32, I32, I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, trace, (I32, I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, trace_num, (I32, I32, I64)) + HOOK_API_DEFINITION(I64, trace_float, (I32, I32, I64)) + HOOK_API_DEFINITION(I64, otxn_burden, ()) + HOOK_API_DEFINITION(I64, otxn_field, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, otxn_generation, ()) + HOOK_API_DEFINITION(I64, otxn_id, (I32, I32, I32)) + HOOK_API_DEFINITION(I64, otxn_type, ()) + HOOK_API_DEFINITION(I64, otxn_slot, (I32)) + HOOK_API_DEFINITION(I64, otxn_param, (I32, I32, I32, I32)) + HOOK_API_DEFINITION(I64, meta_slot, (I32)) -// featureHooks1 -static const APIWhitelist import_whitelist_1{ - // clang-format off - HOOK_API_DEFINITION(I64, xpop_slot, (I32, I32)), + if (rules.enabled(featureHooksUpdate1)) + HOOK_API_DEFINITION(I64, xpop_slot, (I32, I32)) + + return whitelist; // clang-format on -}; +} #undef HOOK_API_DEFINITION #undef I32 #undef I64 + +enum GuardRulesVersion : uint64_t { + GuardRuleFix20250131 = 0x00000001, +}; + +inline uint64_t +getGuardRulesVersion(Rules const& rules) +{ + uint64_t version = 0; + if (rules.enabled(fix20250131)) + version |= GuardRuleFix20250131; + return version; +} + }; // namespace hook_api #endif diff --git a/src/ripple/app/hook/Guard.h b/src/ripple/app/hook/Guard.h index f395af448..051d82128 100644 --- a/src/ripple/app/hook/Guard.h +++ b/src/ripple/app/hook/Guard.h @@ -634,7 +634,7 @@ check_guard( } else if (fc_type == 10) // memory.copy { - if (rulesVersion & 0x02U) + if (rulesVersion & hook_api::GuardRuleFix20250131) GUARD_ERROR("Memory.copy instruction is not allowed."); REQUIRE(2); @@ -642,7 +642,7 @@ check_guard( } else if (fc_type == 11) // memory.fill { - if (rulesVersion & 0x02U) + if (rulesVersion & hook_api::GuardRuleFix20250131) GUARD_ERROR("Memory.fill instruction is not allowed."); ADVANCE(1); @@ -826,6 +826,7 @@ validateGuards( std::vector const& wasm, GuardLog guardLog, std::string guardLogAccStr, + hook_api::APIWhitelist const import_whitelist, /* RH NOTE: * rules version is a bit field, so rule update 1 is 0x01, update 2 is 0x02 * and update 3 is 0x04 ideally at rule version 3 all bits so far are set @@ -835,7 +836,7 @@ validateGuards( * might have unforeseen consequences, without also rolling back further * changes that are fine. */ - uint64_t rulesVersion = 0) + uint64_t rulesVersion = 0x00) { uint64_t byteCount = wasm.size(); @@ -1020,31 +1021,24 @@ validateGuards( int type_idx = parseLeb128(wasm, i, &i); CHECK_SHORT_HOOK(); + auto it = import_whitelist.find(import_name); + auto it_end = import_whitelist.end(); + bool found_in_whitelist = (it != it_end); + if (import_name == "_g") { guard_import_number = func_upto; } - else if ( - hook_api::import_whitelist.find(import_name) == - hook_api::import_whitelist.end()) + if (!found_in_whitelist) { - if (rulesVersion > 0 && - hook_api::import_whitelist_1.find(import_name) != - hook_api::import_whitelist_1.end()) - { - // PASS, this is a version 1 api - } - else - { - GUARDLOG(hook::log::IMPORT_ILLEGAL) - << "Malformed transaction. " - << "Hook attempted to import a function that does " - "not " - << "appear in the hook_api function set: `" - << import_name << "`" - << "\n"; - return {}; - } + GUARDLOG(hook::log::IMPORT_ILLEGAL) + << "Malformed transaction. " + << "Hook attempted to import a function that does " + "not " + << "appear in the hook_api function set: `" + << import_name << "`" + << "\n"; + return {}; } // add to import map @@ -1259,11 +1253,7 @@ validateGuards( for (auto const& [import_idx, api_name] : usage->second) { auto const& api_signature = - hook_api::import_whitelist.find(api_name) != - hook_api::import_whitelist.end() - ? hook_api::import_whitelist.find(api_name)->second - : hook_api::import_whitelist_1.find(api_name) - ->second; + import_whitelist.find(api_name)->second; if (!first_signature) { diff --git a/src/ripple/app/hook/guard_checker.cpp b/src/ripple/app/hook/guard_checker.cpp index f20d24617..1fc61ea2d 100644 --- a/src/ripple/app/hook/guard_checker.cpp +++ b/src/ripple/app/hook/guard_checker.cpp @@ -1,3 +1,5 @@ +#define GUARD_CHECKER_BUILD +#include "Enum.h" #include "Guard.h" #include #include @@ -79,7 +81,15 @@ main(int argc, char** argv) close(fd); - auto result = validateGuards(hook, std::cout, "", 3); + // Dummy rules for guard checker build + hook_api::Rules rules; + + auto result = validateGuards( + hook, + std::cout, + "", + hook_api::getImportWhitelist(rules), + hook_api::getGuardRulesVersion(rules)); if (!result) { diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 37a436fee..1a78e0815 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -605,8 +606,8 @@ Change::activateXahauGenesis() wasmBytes, // wasm to verify loggerStream, "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", - (ctx_.view().rules().enabled(featureHooksUpdate1) ? 1 : 0) + - (ctx_.view().rules().enabled(fix20250131) ? 2 : 0)); + hook_api::getImportWhitelist(ctx_.view().rules()), + hook_api::getGuardRulesVersion(ctx_.view().rules())); if (!result) { diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp index 9ca829557..2b35f2a59 100644 --- a/src/ripple/app/tx/impl/SetHook.cpp +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -490,8 +490,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) hook, // wasm to verify logger, hsacc, - (ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) + - (ctx.rules.enabled(fix20250131) ? 2 : 0)); + hook_api::getImportWhitelist(ctx.rules), + hook_api::getGuardRulesVersion(ctx.rules)); if (ctx.j.trace()) { diff --git a/src/test/app/SetHook_test.cpp b/src/test/app/SetHook_test.cpp index 0c7be29e8..81cb317df 100644 --- a/src/test/app/SetHook_test.cpp +++ b/src/test/app/SetHook_test.cpp @@ -6872,7 +6872,10 @@ public: std::vector const keys = { "ED74D4036C6591A4BDF9C54CEFA39B996A5DCE5F86D11FDA1874481CE9D5A1CDC" "1"}; - Env env{*this, network::makeNetworkVLConfig(21337, keys)}; + Env env{ + *this, + network::makeNetworkVLConfig(21337, keys), + features - featureHooksUpdate1}; auto const master = Account("masterpassphrase"); env(noop(master), fee(10'000'000'000), ter(tesSUCCESS)); @@ -6950,6 +6953,16 @@ public: } )[test.hook]"]; + // before featureHooksUpdate1 + env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + M("set xpop_slot (disabled)"), + HSFEE, + ter(temMALFORMED)); + env.close(); + + env.enableFeature(featureHooksUpdate1); + env.close(); + // install the hook on alice env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), M("set xpop_slot"), From fb7a0d71de3261b5a477bc75b0cff9e5a065be33 Mon Sep 17 00:00:00 2001 From: tequ Date: Sun, 15 Feb 2026 19:08:44 +0900 Subject: [PATCH 2/5] Hook API Refactor3: Consolidate the Hook API definitions from Enum.h and ApplyHook.h into a single file. (#622) --- .../workflows/verify-generated-headers.yml | 13 +- hook/extern.h | 22 +- hook/generate_extern.sh | 137 +----- src/ripple/app/hook/Enum.h | 117 ++--- src/ripple/app/hook/Macro.h | 81 +-- src/ripple/app/hook/applyHook.h | 462 +----------------- src/ripple/app/hook/hook_api.macro | 369 ++++++++++++++ src/ripple/app/hook/impl/applyHook.cpp | 22 +- 8 files changed, 496 insertions(+), 727 deletions(-) create mode 100644 src/ripple/app/hook/hook_api.macro diff --git a/.github/workflows/verify-generated-headers.yml b/.github/workflows/verify-generated-headers.yml index 4120f7b7f..723e7181b 100644 --- a/.github/workflows/verify-generated-headers.yml +++ b/.github/workflows/verify-generated-headers.yml @@ -18,12 +18,23 @@ jobs: generator: bash ./hook/generate_sfcodes.sh - target: hook/tts.h generator: ./hook/generate_tts.sh - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + env: + CLANG_VERSION: 10 name: ${{ matrix.target }} steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Download and install clang-format + run: | + sudo apt-get update -y + sudo apt-get install -y libtinfo5 + curl -LO https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.1/clang+llvm-10.0.1-x86_64-linux-gnu-ubuntu-16.04.tar.xz + tar -xf clang+llvm-10.0.1-x86_64-linux-gnu-ubuntu-16.04.tar.xz + sudo mv clang+llvm-10.0.1-x86_64-linux-gnu-ubuntu-16.04 /opt/clang-10 + sudo ln -s /opt/clang-10/bin/clang-format /usr/local/bin/clang-format-10 + - name: Verify ${{ matrix.target }} run: | set -euo pipefail diff --git a/hook/extern.h b/hook/extern.h index ee34af6ff..667cec2a4 100644 --- a/hook/extern.h +++ b/hook/extern.h @@ -82,7 +82,7 @@ sto_erase( uint32_t field_id); extern int64_t -etxn_burden(void); +etxn_burden(); extern int64_t etxn_details(uint32_t write_ptr, uint32_t write_len); @@ -94,7 +94,7 @@ extern int64_t etxn_reserve(uint32_t count); extern int64_t -etxn_generation(void); +etxn_generation(); extern int64_t etxn_nonce(uint32_t write_ptr, uint32_t write_len); @@ -149,7 +149,7 @@ extern int64_t float_divide(int64_t float1, int64_t float2); extern int64_t -float_one(void); +float_one(); extern int64_t float_mantissa(int64_t float1); @@ -167,13 +167,13 @@ extern int64_t float_root(int64_t float1, uint32_t n); extern int64_t -fee_base(void); +fee_base(); extern int64_t -ledger_seq(void); +ledger_seq(); extern int64_t -ledger_last_time(void); +ledger_last_time(); extern int64_t ledger_last_hash(uint32_t write_ptr, uint32_t write_len); @@ -213,13 +213,13 @@ hook_param( uint32_t read_len); extern int64_t -hook_again(void); +hook_again(); extern int64_t hook_skip(uint32_t read_ptr, uint32_t read_len, uint32_t flags); extern int64_t -hook_pos(void); +hook_pos(); extern int64_t slot(uint32_t write_ptr, uint32_t write_len, uint32_t slot); @@ -299,19 +299,19 @@ extern int64_t trace_float(uint32_t read_ptr, uint32_t read_len, int64_t float1); extern int64_t -otxn_burden(void); +otxn_burden(); extern int64_t otxn_field(uint32_t write_ptr, uint32_t write_len, uint32_t field_id); extern int64_t -otxn_generation(void); +otxn_generation(); extern int64_t otxn_id(uint32_t write_ptr, uint32_t write_len, uint32_t flags); extern int64_t -otxn_type(void); +otxn_type(); extern int64_t otxn_slot(uint32_t slot_no); diff --git a/hook/generate_extern.sh b/hook/generate_extern.sh index 2e9598ad0..0f05dac52 100755 --- a/hook/generate_extern.sh +++ b/hook/generate_extern.sh @@ -4,7 +4,7 @@ set -eu SCRIPT_DIR=$(dirname "$0") SCRIPT_DIR=$(cd "$SCRIPT_DIR" && pwd) -APPLY_HOOK="$SCRIPT_DIR/../src/ripple/app/hook/applyHook.h" +APPLY_HOOK="$SCRIPT_DIR/../src/ripple/app/hook/hook_api.macro" { echo '// For documentation please see: https://xrpl-hooks.readme.io/reference/' @@ -19,127 +19,36 @@ APPLY_HOOK="$SCRIPT_DIR/../src/ripple/app/hook/applyHook.h" return s; } - function emit(ret, name, argc, argt, argn) { - attr = (name == "_g") ? " __attribute__((noduplicate))" : ""; - if (!first) - printf("\n"); - first = 0; - printf("extern %s%s\n", ret, attr); - if (argc == 0) { - printf("%s(void);\n", name); - return; - } - if (argc <= 3) { - line = argt[1] " " argn[1]; - for (i = 2; i <= argc; ++i) - line = line ", " argt[i] " " argn[i]; - printf("%s(%s);\n", name, line); - return; - } - printf("%s(\n", name); - for (i = 1; i <= argc; ++i) { - sep = (i < argc) ? "," : ");"; - printf(" %s %s%s\n", argt[i], argn[i], sep); - } - } - - function process(buffer, kind, payload, parts, n, i, arg, tokens, argc, argt, argn) { - if (kind == "func") - sub(/^DECLARE_HOOK_FUNCTION[[:space:]]*\(/, "", buffer); - else - sub(/^DECLARE_HOOK_FUNCNARG[[:space:]]*\(/, "", buffer); - buffer = trim(buffer); - sub(/\)[[:space:]]*$/, "", buffer); - n = split(buffer, parts, ","); - for (i = 1; i <= n; ++i) - parts[i] = trim(parts[i]); - ret = parts[1]; - name = parts[2]; - argc = 0; - delete argt; - delete argn; - for (i = 3; i <= n; ++i) { - arg = parts[i]; - if (arg == "") - continue; - split(arg, tokens, /[[:space:]]+/); - if (length(tokens) < 2) - continue; - ++argc; - argt[argc] = tokens[1]; - argn[argc] = tokens[2]; - } - emit(ret, name, argc, argt, argn); - } - - BEGIN { - first = 1; - in_block = 0; - in_macro = 0; - } - { line = $0; - if (in_block) { - if (line ~ /\*\//) { - sub(/.*\*\//, "", line); - in_block = 0; - } - else - next; - } - while (line ~ /\/\*/) { - if (line ~ /\/\*.*\*\//) { - gsub(/\/\*.*\*\//, "", line); - } - else { - sub(/\/\*.*/, "", line); - in_block = 1; - break; - } - } - sub(/\/\/.*$/, "", line); - line = trim(line); - if (line == "") - next; - - if (!in_macro && line ~ /^DECLARE_HOOK_FUNCTION\(/) { - buffer = line; - kind = "func"; - if (line ~ /\);[[:space:]]*$/) { - sub(/\);[[:space:]]*$/, "", buffer); - process(buffer, kind); - } - else - in_macro = 1; + + # Skip block comments + if (line ~ /\/\*/) { next; } - if (!in_macro && line ~ /^DECLARE_HOOK_FUNCNARG\(/) { - buffer = line; - kind = "narg"; - if (line ~ /\);[[:space:]]*$/) { - sub(/\);[[:space:]]*$/, "", buffer); - process(buffer, kind); + + # Look for comment lines that start with // and contain function signature + if (line ~ /^[[:space:]]*\/\/[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(/) { + # Remove leading // and trim + sub(/^[[:space:]]*\/\/[[:space:]]*/, "", line); + line = trim(line); + + # Check if function name is "_g" to add attribute + if (line ~ /[[:space:]]+_g[[:space:]]*\(/) { + # Insert __attribute__((noduplicate)) before _g + sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line); } - else - in_macro = 1; - next; + + # printf("\n"); + + printf("extern %s\n\n", line); } - if (in_macro) { - buffer = buffer " " line; - if (line ~ /\);[[:space:]]*$/) { - sub(/\);[[:space:]]*$/, "", buffer); - process(buffer, kind); - in_macro = 0; - } - } - } - - END { - printf("\n"); } ' "$APPLY_HOOK" echo '#define HOOK_EXTERN' echo '#endif // HOOK_EXTERN' -} +} | ( + cd "$SCRIPT_DIR/.." + clang-format --style=file - +) diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 0ef817def..84c654f3e 100644 --- a/src/ripple/app/hook/Enum.h +++ b/src/ripple/app/hook/Enum.h @@ -7,17 +7,19 @@ #define HOOKENUM_INCLUDED 1 #ifndef GUARD_CHECKER_BUILD +#include #include #include #else -// Override Feature and Rules for guard checker build -#define featureHooksUpdate1 1 -#define fix20250131 1 +// Override uint256, Feature and Rules for guard checker build +#define uint256 std::string +#define featureHooksUpdate1 "1" +#define fix20250131 "1" namespace hook_api { struct Rules { constexpr bool - enabled(int feature) const + enabled(const uint256& feature) const { return true; } @@ -387,13 +389,6 @@ const uint8_t max_emit = 255; const uint8_t max_params = 16; const double fee_base_multiplier = 1.1f; -#define I32 0x7FU -#define I64 0x7EU - -#define HOOK_WRAP_PARAMS(...) __VA_ARGS__ -#define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE) \ - whitelist[#FUNCTION_NAME] = {RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE}; - using APIWhitelist = std::map>; // RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and @@ -403,86 +398,32 @@ inline APIWhitelist getImportWhitelist(Rules const& rules) { APIWhitelist whitelist; - // clang-format off - HOOK_API_DEFINITION(I32, _g, (I32, I32)) - HOOK_API_DEFINITION(I64, accept, (I32, I32, I64)) - HOOK_API_DEFINITION(I64, rollback, (I32, I32, I64)) - HOOK_API_DEFINITION(I64, util_raddr, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, util_accid, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, util_verify, (I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, util_sha512h, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, util_keylet, (I32, I32, I32, I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, sto_validate, (I32, I32)) - HOOK_API_DEFINITION(I64, sto_subfield, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, sto_subarray, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, sto_emplace, (I32, I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, sto_erase, (I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, etxn_burden, ()) - HOOK_API_DEFINITION(I64, etxn_details, (I32, I32)) - HOOK_API_DEFINITION(I64, etxn_fee_base, (I32, I32)) - HOOK_API_DEFINITION(I64, etxn_reserve, (I32)) - HOOK_API_DEFINITION(I64, etxn_generation, ()) - HOOK_API_DEFINITION(I64, etxn_nonce, (I32, I32)) - HOOK_API_DEFINITION(I64, emit, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, float_set, (I32, I64)) - HOOK_API_DEFINITION(I64, float_multiply, (I64, I64)) - HOOK_API_DEFINITION(I64, float_mulratio, (I64, I32, I32, I32)) - HOOK_API_DEFINITION(I64, float_negate, (I64)) - HOOK_API_DEFINITION(I64, float_compare, (I64, I64, I32)) - HOOK_API_DEFINITION(I64, float_sum, (I64, I64)) - HOOK_API_DEFINITION(I64, float_sto, (I32, I32, I32, I32, I32, I32, I64, I32)) - HOOK_API_DEFINITION(I64, float_sto_set, (I32, I32)) - HOOK_API_DEFINITION(I64, float_invert, (I64)) - HOOK_API_DEFINITION(I64, float_divide, (I64, I64)) - HOOK_API_DEFINITION(I64, float_one, ()) - HOOK_API_DEFINITION(I64, float_mantissa, (I64)) - HOOK_API_DEFINITION(I64, float_sign, (I64)) - HOOK_API_DEFINITION(I64, float_int, (I64, I32, I32)) - HOOK_API_DEFINITION(I64, float_log, (I64)) - HOOK_API_DEFINITION(I64, float_root, (I64, I32)) - HOOK_API_DEFINITION(I64, fee_base, ()) - HOOK_API_DEFINITION(I64, ledger_seq, ()) - HOOK_API_DEFINITION(I64, ledger_last_time, ()) - HOOK_API_DEFINITION(I64, ledger_last_hash, (I32, I32)) - HOOK_API_DEFINITION(I64, ledger_nonce, (I32, I32)) - HOOK_API_DEFINITION(I64, ledger_keylet, (I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, hook_account, (I32, I32)) - HOOK_API_DEFINITION(I64, hook_hash, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, hook_param_set, (I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, hook_param, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, hook_again, ()) - HOOK_API_DEFINITION(I64, hook_skip, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, hook_pos, ()) - HOOK_API_DEFINITION(I64, slot, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, slot_clear, (I32)) - HOOK_API_DEFINITION(I64, slot_count, (I32)) - HOOK_API_DEFINITION(I64, slot_set, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, slot_size, (I32)) - HOOK_API_DEFINITION(I64, slot_subarray, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, slot_subfield, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, slot_type, (I32, I32)) - HOOK_API_DEFINITION(I64, slot_float, (I32)) - HOOK_API_DEFINITION(I64, state_set, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, state_foreign_set, (I32, I32, I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, state, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, state_foreign, (I32, I32, I32, I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, trace, (I32, I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, trace_num, (I32, I32, I64)) - HOOK_API_DEFINITION(I64, trace_float, (I32, I32, I64)) - HOOK_API_DEFINITION(I64, otxn_burden, ()) - HOOK_API_DEFINITION(I64, otxn_field, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, otxn_generation, ()) - HOOK_API_DEFINITION(I64, otxn_id, (I32, I32, I32)) - HOOK_API_DEFINITION(I64, otxn_type, ()) - HOOK_API_DEFINITION(I64, otxn_slot, (I32)) - HOOK_API_DEFINITION(I64, otxn_param, (I32, I32, I32, I32)) - HOOK_API_DEFINITION(I64, meta_slot, (I32)) - if (rules.enabled(featureHooksUpdate1)) - HOOK_API_DEFINITION(I64, xpop_slot, (I32, I32)) +#pragma push_macro("HOOK_API_DEFINITION") +#undef HOOK_API_DEFINITION + +#define int64_t 0x7EU +#define int32_t 0x7FU +#define uint32_t 0x7FU + +#define HOOK_WRAP_PARAMS(...) __VA_ARGS__ + +#define HOOK_API_DEFINITION( \ + RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \ + if (AMENDMENT == uint256{} || rules.enabled(AMENDMENT)) \ + whitelist[#FUNCTION_NAME] = { \ + RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE}; + +#include "hook_api.macro" + +#undef HOOK_API_DEFINITION +#undef HOOK_WRAP_PARAMS +#undef int64_t +#undef int32_t +#undef uint32_t +#pragma pop_macro("HOOK_API_DEFINITION") return whitelist; - // clang-format on } #undef HOOK_API_DEFINITION diff --git a/src/ripple/app/hook/Macro.h b/src/ripple/app/hook/Macro.h index 5f309b2e3..ebf3499b2 100644 --- a/src/ripple/app/hook/Macro.h +++ b/src/ripple/app/hook/Macro.h @@ -25,7 +25,8 @@ _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, N, ...) \ N #define VA_NARGS(__drop, ...) \ - VA_NARGS_IMPL(__VA_ARGS__, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + VA_NARGS_IMPL( \ + __VA_OPT__(__VA_ARGS__, ) 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) #define FIRST(a, b) a #define SECOND(a, b) b #define STRIP_TYPES(...) FOR_VARS(SECOND, 0, __VA_ARGS__) @@ -88,30 +89,18 @@ #define WASM_VAL_TYPE(T, b) CAT2(TYP_, T) -#define DECLARE_HOOK_FUNCTION(R, F, ...) \ - R F(hook::HookContext& hookCtx, \ - WasmEdge_CallingFrameContext const& frameCtx, \ - __VA_ARGS__); \ - extern WasmEdge_Result WasmFunction##F( \ - void* data_ptr, \ - const WasmEdge_CallingFrameContext* frameCtx, \ - const WasmEdge_Value* in, \ - WasmEdge_Value* out); \ - extern WasmEdge_ValType WasmFunctionParams##F[]; \ - extern WasmEdge_ValType WasmFunctionResult##F[]; \ - extern WasmEdge_FunctionTypeContext* WasmFunctionType##F; \ - extern WasmEdge_String WasmFunctionName##F; - -#define DECLARE_HOOK_FUNCNARG(R, F) \ - R F(hook::HookContext& hookCtx, \ - WasmEdge_CallingFrameContext const& frameCtx); \ - extern WasmEdge_Result WasmFunction##F( \ - void* data_ptr, \ - const WasmEdge_CallingFrameContext* frameCtx, \ - const WasmEdge_Value* in, \ - WasmEdge_Value* out); \ - extern WasmEdge_ValType WasmFunctionResult##F[]; \ - extern WasmEdge_FunctionTypeContext* WasmFunctionType##F; \ +#define DECLARE_HOOK_FUNCTION(R, F, ...) \ + R F(hook::HookContext& hookCtx, \ + WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \ + COMMA __VA_ARGS__)); \ + extern WasmEdge_Result WasmFunction##F( \ + void* data_ptr, \ + const WasmEdge_CallingFrameContext* frameCtx, \ + const WasmEdge_Value* in, \ + WasmEdge_Value* out); \ + extern WasmEdge_ValType WasmFunctionParams##F[]; \ + extern WasmEdge_ValType WasmFunctionResult##F[]; \ + extern WasmEdge_FunctionTypeContext* WasmFunctionType##F; \ extern WasmEdge_String WasmFunctionName##F; #define DEFINE_HOOK_FUNCTION(R, F, ...) \ @@ -121,61 +110,35 @@ const WasmEdge_Value* in, \ WasmEdge_Value* out) \ { \ - int _stack = 0; \ - FOR_VARS(VAR_ASSIGN, 2, __VA_ARGS__); \ + __VA_OPT__(int _stack = 0;) \ + __VA_OPT__(FOR_VARS(VAR_ASSIGN, 2, __VA_ARGS__);) \ hook::HookContext* hookCtx = \ reinterpret_cast(data_ptr); \ R return_code = hook_api::F( \ *hookCtx, \ - *const_cast(frameCtx), \ - STRIP_TYPES(__VA_ARGS__)); \ + *const_cast(frameCtx) \ + __VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \ if (return_code == RC_ROLLBACK || return_code == RC_ACCEPT) \ return WasmEdge_Result_Terminate; \ out[0] = RET_ASSIGN(R, return_code); \ return WasmEdge_Result_Success; \ }; \ WasmEdge_ValType hook_api::WasmFunctionParams##F[] = { \ - FOR_VARS(WASM_VAL_TYPE, 0, __VA_ARGS__)}; \ + __VA_OPT__(FOR_VARS(WASM_VAL_TYPE, 0, __VA_ARGS__))}; \ WasmEdge_ValType hook_api::WasmFunctionResult##F[1] = { \ WASM_VAL_TYPE(R, dummy)}; \ WasmEdge_FunctionTypeContext* hook_api::WasmFunctionType##F = \ WasmEdge_FunctionTypeCreate( \ WasmFunctionParams##F, \ - VA_NARGS(NULL, __VA_ARGS__), \ + VA_NARGS(NULL __VA_OPT__(, __VA_ARGS__)), \ WasmFunctionResult##F, \ 1); \ WasmEdge_String hook_api::WasmFunctionName##F = \ WasmEdge_StringCreateByCString(#F); \ R hook_api::F( \ hook::HookContext& hookCtx, \ - WasmEdge_CallingFrameContext const& frameCtx, \ - __VA_ARGS__) - -#define DEFINE_HOOK_FUNCNARG(R, F) \ - WasmEdge_Result hook_api::WasmFunction##F( \ - void* data_ptr, \ - const WasmEdge_CallingFrameContext* frameCtx, \ - const WasmEdge_Value* in, \ - WasmEdge_Value* out) \ - { \ - hook::HookContext* hookCtx = \ - reinterpret_cast(data_ptr); \ - R return_code = hook_api::F( \ - *hookCtx, *const_cast(frameCtx)); \ - if (return_code == RC_ROLLBACK || return_code == RC_ACCEPT) \ - return WasmEdge_Result_Terminate; \ - out[0] = CAT2(RET_, R(return_code)); \ - return WasmEdge_Result_Success; \ - }; \ - WasmEdge_ValType hook_api::WasmFunctionResult##F[1] = { \ - WASM_VAL_TYPE(R, dummy)}; \ - WasmEdge_FunctionTypeContext* hook_api::WasmFunctionType##F = \ - WasmEdge_FunctionTypeCreate({}, 0, WasmFunctionResult##F, 1); \ - WasmEdge_String hook_api::WasmFunctionName##F = \ - WasmEdge_StringCreateByCString(#F); \ - R hook_api::F( \ - hook::HookContext& hookCtx, \ - WasmEdge_CallingFrameContext const& frameCtx) + WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \ + COMMA __VA_ARGS__)) #define HOOK_SETUP() \ try \ diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 2c1f250bb..3fa02cbe6 100644 --- a/src/ripple/app/hook/applyHook.h +++ b/src/ripple/app/hook/applyHook.h @@ -61,365 +61,20 @@ namespace hook_api { if (HOOK_DBG) \ fprintf -DECLARE_HOOK_FUNCTION(int32_t, _g, uint32_t guard_id, uint32_t maxiter); +#pragma push_macro("HOOK_API_DEFINITION") +#undef HOOK_API_DEFINITION -DECLARE_HOOK_FUNCTION( - int64_t, - accept, - uint32_t read_ptr, - uint32_t read_len, - int64_t error_code); -DECLARE_HOOK_FUNCTION( - int64_t, - rollback, - uint32_t read_ptr, - uint32_t read_len, - int64_t error_code); +#define HOOK_WRAP_PARAMS(...) __VA_ARGS__ +#define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, ...) \ + DECLARE_HOOK_FUNCTION( \ + RETURN_TYPE, FUNCTION_NAME, HOOK_WRAP_PARAMS PARAMS_TUPLE); -DECLARE_HOOK_FUNCTION( - int64_t, - util_raddr, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len); -DECLARE_HOOK_FUNCTION( - int64_t, - util_accid, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len); -DECLARE_HOOK_FUNCTION( - int64_t, - util_verify, - uint32_t dread_ptr, - uint32_t dread_len, - uint32_t sread_ptr, - uint32_t sread_len, - uint32_t kread_ptr, - uint32_t kread_len); -DECLARE_HOOK_FUNCTION( - int64_t, - util_sha512h, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len); -DECLARE_HOOK_FUNCTION( - int64_t, - util_keylet, - uint32_t write_ptr, - uint32_t write_len, - uint32_t keylet_type, - uint32_t a, - uint32_t b, - uint32_t c, - uint32_t d, - uint32_t e, - uint32_t f); +#include -DECLARE_HOOK_FUNCTION( - int64_t, - sto_validate, - uint32_t tread_ptr, - uint32_t tread_len); -DECLARE_HOOK_FUNCTION( - int64_t, - sto_subfield, - uint32_t read_ptr, - uint32_t read_len, - uint32_t field_id); -DECLARE_HOOK_FUNCTION( - int64_t, - sto_subarray, - uint32_t read_ptr, - uint32_t read_len, - uint32_t array_id); -DECLARE_HOOK_FUNCTION( - int64_t, - sto_emplace, - uint32_t write_ptr, - uint32_t write_len, - uint32_t sread_ptr, - uint32_t sread_len, - uint32_t fread_ptr, - uint32_t fread_len, - uint32_t field_id); -DECLARE_HOOK_FUNCTION( - int64_t, - sto_erase, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len, - uint32_t field_id); +#undef HOOK_API_DEFINITION +#undef HOOK_WRAP_PARAMS +#pragma pop_macro("HOOK_API_DEFINITION") -DECLARE_HOOK_FUNCNARG(int64_t, etxn_burden); -DECLARE_HOOK_FUNCTION( - int64_t, - etxn_details, - uint32_t write_ptr, - uint32_t write_len); -DECLARE_HOOK_FUNCTION( - int64_t, - etxn_fee_base, - uint32_t read_ptr, - uint32_t read_len); -DECLARE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count); -DECLARE_HOOK_FUNCNARG(int64_t, etxn_generation); -DECLARE_HOOK_FUNCTION( - int64_t, - etxn_nonce, - uint32_t write_ptr, - uint32_t write_len); -DECLARE_HOOK_FUNCTION( - int64_t, - emit, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len); - -DECLARE_HOOK_FUNCTION(int64_t, float_set, int32_t exponent, int64_t mantissa); -DECLARE_HOOK_FUNCTION(int64_t, float_multiply, int64_t float1, int64_t float2); -DECLARE_HOOK_FUNCTION( - int64_t, - float_mulratio, - int64_t float1, - uint32_t round_up, - uint32_t numerator, - uint32_t denominator); -DECLARE_HOOK_FUNCTION(int64_t, float_negate, int64_t float1); -DECLARE_HOOK_FUNCTION( - int64_t, - float_compare, - int64_t float1, - int64_t float2, - uint32_t mode); -DECLARE_HOOK_FUNCTION(int64_t, float_sum, int64_t float1, int64_t float2); -DECLARE_HOOK_FUNCTION( - int64_t, - float_sto, - uint32_t write_ptr, - uint32_t write_len, - uint32_t cread_ptr, - uint32_t cread_len, - uint32_t iread_ptr, - uint32_t iread_len, - int64_t float1, - uint32_t field_code); -DECLARE_HOOK_FUNCTION( - int64_t, - float_sto_set, - uint32_t read_ptr, - uint32_t read_len); -DECLARE_HOOK_FUNCTION(int64_t, float_invert, int64_t float1); -DECLARE_HOOK_FUNCTION(int64_t, float_divide, int64_t float1, int64_t float2); -DECLARE_HOOK_FUNCNARG(int64_t, float_one); -DECLARE_HOOK_FUNCTION(int64_t, float_mantissa, int64_t float1); -DECLARE_HOOK_FUNCTION(int64_t, float_sign, int64_t float1); -DECLARE_HOOK_FUNCTION( - int64_t, - float_int, - int64_t float1, - uint32_t decimal_places, - uint32_t abs); -DECLARE_HOOK_FUNCTION(int64_t, float_log, int64_t float1); -DECLARE_HOOK_FUNCTION(int64_t, float_root, int64_t float1, uint32_t n); - -DECLARE_HOOK_FUNCNARG(int64_t, fee_base); -DECLARE_HOOK_FUNCNARG(int64_t, ledger_seq); -DECLARE_HOOK_FUNCNARG(int64_t, ledger_last_time); -DECLARE_HOOK_FUNCTION( - int64_t, - ledger_last_hash, - uint32_t write_ptr, - uint32_t write_len); -DECLARE_HOOK_FUNCTION( - int64_t, - ledger_nonce, - uint32_t write_ptr, - uint32_t write_len); -DECLARE_HOOK_FUNCTION( - int64_t, - ledger_keylet, - uint32_t write_ptr, - uint32_t write_len, - uint32_t lread_ptr, - uint32_t lread_len, - uint32_t hread_ptr, - uint32_t hread_len); - -DECLARE_HOOK_FUNCTION( - int64_t, - hook_account, - uint32_t write_ptr, - uint32_t write_len); -DECLARE_HOOK_FUNCTION( - int64_t, - hook_hash, - uint32_t write_ptr, - uint32_t write_len, - int32_t hook_no); -DECLARE_HOOK_FUNCTION( - int64_t, - hook_param_set, - uint32_t read_ptr, - uint32_t read_len, - uint32_t kread_ptr, - uint32_t kread_len, - uint32_t hread_ptr, - uint32_t hread_len); -DECLARE_HOOK_FUNCTION( - int64_t, - hook_param, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len); -DECLARE_HOOK_FUNCNARG(int64_t, hook_again); -DECLARE_HOOK_FUNCTION( - int64_t, - hook_skip, - uint32_t read_ptr, - uint32_t read_len, - uint32_t flags); -DECLARE_HOOK_FUNCNARG(int64_t, hook_pos); - -DECLARE_HOOK_FUNCTION( - int64_t, - slot, - uint32_t write_ptr, - uint32_t write_len, - uint32_t slot); -DECLARE_HOOK_FUNCTION(int64_t, slot_clear, uint32_t slot); -DECLARE_HOOK_FUNCTION(int64_t, slot_count, uint32_t slot); -DECLARE_HOOK_FUNCTION( - int64_t, - slot_set, - uint32_t read_ptr, - uint32_t read_len, - uint32_t slot); -DECLARE_HOOK_FUNCTION(int64_t, slot_size, uint32_t slot); -DECLARE_HOOK_FUNCTION( - int64_t, - slot_subarray, - uint32_t parent_slot, - uint32_t array_id, - uint32_t new_slot); -DECLARE_HOOK_FUNCTION( - int64_t, - slot_subfield, - uint32_t parent_slot, - uint32_t field_id, - uint32_t new_slot); -DECLARE_HOOK_FUNCTION(int64_t, slot_type, uint32_t slot_no, uint32_t flags); -DECLARE_HOOK_FUNCTION(int64_t, slot_float, uint32_t slot_no); - -DECLARE_HOOK_FUNCTION( - int64_t, - state_set, - uint32_t read_ptr, - uint32_t read_len, - uint32_t kread_ptr, - uint32_t kread_len); -DECLARE_HOOK_FUNCTION( - int64_t, - state_foreign_set, - uint32_t read_ptr, - uint32_t read_len, - uint32_t kread_ptr, - uint32_t kread_len, - uint32_t nread_ptr, - uint32_t nread_len, - uint32_t aread_ptr, - uint32_t aread_len); -DECLARE_HOOK_FUNCTION( - int64_t, - state, - uint32_t write_ptr, - uint32_t write_len, - uint32_t kread_ptr, - uint32_t kread_len); -DECLARE_HOOK_FUNCTION( - int64_t, - state_foreign, - uint32_t write_ptr, - uint32_t write_len, - uint32_t kread_ptr, - uint32_t kread_len, - uint32_t nread_ptr, - uint32_t nread_len, - uint32_t aread_ptr, - uint32_t aread_len); - -DECLARE_HOOK_FUNCTION( - int64_t, - trace, - uint32_t mread_ptr, - uint32_t mread_len, - uint32_t dread_ptr, - uint32_t dread_len, - uint32_t as_hex); -DECLARE_HOOK_FUNCTION( - int64_t, - trace_num, - uint32_t read_ptr, - uint32_t read_len, - int64_t number); -DECLARE_HOOK_FUNCTION( - int64_t, - trace_float, - uint32_t read_ptr, - uint32_t read_len, - int64_t float1); - -DECLARE_HOOK_FUNCNARG(int64_t, otxn_burden); -DECLARE_HOOK_FUNCTION( - int64_t, - otxn_field, - uint32_t write_ptr, - uint32_t write_len, - uint32_t field_id); -DECLARE_HOOK_FUNCNARG(int64_t, otxn_generation); -DECLARE_HOOK_FUNCTION( - int64_t, - otxn_id, - uint32_t write_ptr, - uint32_t write_len, - uint32_t flags); -DECLARE_HOOK_FUNCNARG(int64_t, otxn_type); -DECLARE_HOOK_FUNCTION(int64_t, otxn_slot, uint32_t slot_no); -DECLARE_HOOK_FUNCTION( - int64_t, - otxn_param, - uint32_t write_ptr, - uint32_t write_len, - uint32_t read_ptr, - uint32_t read_len); - -DECLARE_HOOK_FUNCTION(int64_t, meta_slot, uint32_t slot_no); -DECLARE_HOOK_FUNCTION( - int64_t, - xpop_slot, - uint32_t slot_no_tx, - uint32_t slot_no_meta); - -/* - 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, - uint32_t n); DECLARE_HOOK_FUNCTION(int64_t, str_replace, uint32_t - write_ptr, uint32_t write_len, uint32_t hread_ptr, uint32_t hread_len, - uint32_t nread_ptr, - uint32_t nread_len, uint32_t rread_ptr, uint32_t rread_len, uint32_t mode, - uint32_t n); DECLARE_HOOK_FUNCTION(int64_t, str_compare, uint32_t - fread_ptr, uint32_t fread_len, uint32_t sread_ptr, uint32_t sread_len, - uint32_t mode); - DECLARE_HOOK_FUNCTION(int64_t, str_concat, uint32_t write_ptr, - uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint64_t operand, - uint32_t operand_type); -*/ } /* end namespace hook_api */ namespace hook { @@ -792,97 +447,18 @@ public: WasmEdge_LogSetDebugLevel(); - ADD_HOOK_FUNCTION(_g, ctx); - ADD_HOOK_FUNCTION(accept, ctx); - ADD_HOOK_FUNCTION(rollback, ctx); - ADD_HOOK_FUNCTION(util_raddr, ctx); - ADD_HOOK_FUNCTION(util_accid, ctx); - ADD_HOOK_FUNCTION(util_verify, ctx); - ADD_HOOK_FUNCTION(util_sha512h, ctx); - ADD_HOOK_FUNCTION(sto_validate, ctx); - ADD_HOOK_FUNCTION(sto_subfield, ctx); - ADD_HOOK_FUNCTION(sto_subarray, ctx); - ADD_HOOK_FUNCTION(sto_emplace, ctx); - ADD_HOOK_FUNCTION(sto_erase, ctx); - ADD_HOOK_FUNCTION(util_keylet, ctx); +#pragma push_macro("HOOK_API_DEFINITION") +#undef HOOK_API_DEFINITION - ADD_HOOK_FUNCTION(emit, ctx); - ADD_HOOK_FUNCTION(etxn_burden, ctx); - ADD_HOOK_FUNCTION(etxn_fee_base, ctx); - ADD_HOOK_FUNCTION(etxn_details, ctx); - ADD_HOOK_FUNCTION(etxn_reserve, ctx); - ADD_HOOK_FUNCTION(etxn_generation, ctx); - ADD_HOOK_FUNCTION(etxn_nonce, ctx); +#define HOOK_WRAP_PARAMS(...) __VA_ARGS__ +#define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, ...) \ + ADD_HOOK_FUNCTION(FUNCTION_NAME, ctx); - ADD_HOOK_FUNCTION(float_set, ctx); - ADD_HOOK_FUNCTION(float_multiply, ctx); - ADD_HOOK_FUNCTION(float_mulratio, ctx); - ADD_HOOK_FUNCTION(float_negate, ctx); - ADD_HOOK_FUNCTION(float_compare, ctx); - ADD_HOOK_FUNCTION(float_sum, ctx); - ADD_HOOK_FUNCTION(float_sto, ctx); - ADD_HOOK_FUNCTION(float_sto_set, ctx); - ADD_HOOK_FUNCTION(float_invert, ctx); +#include - ADD_HOOK_FUNCTION(float_divide, ctx); - ADD_HOOK_FUNCTION(float_one, ctx); - ADD_HOOK_FUNCTION(float_mantissa, ctx); - ADD_HOOK_FUNCTION(float_sign, ctx); - ADD_HOOK_FUNCTION(float_int, ctx); - ADD_HOOK_FUNCTION(float_log, ctx); - ADD_HOOK_FUNCTION(float_root, ctx); - - ADD_HOOK_FUNCTION(otxn_burden, ctx); - ADD_HOOK_FUNCTION(otxn_generation, ctx); - ADD_HOOK_FUNCTION(otxn_field, ctx); - ADD_HOOK_FUNCTION(otxn_id, ctx); - ADD_HOOK_FUNCTION(otxn_type, ctx); - ADD_HOOK_FUNCTION(otxn_slot, ctx); - ADD_HOOK_FUNCTION(otxn_param, ctx); - - ADD_HOOK_FUNCTION(hook_account, ctx); - ADD_HOOK_FUNCTION(hook_hash, ctx); - ADD_HOOK_FUNCTION(hook_again, ctx); - ADD_HOOK_FUNCTION(fee_base, ctx); - ADD_HOOK_FUNCTION(ledger_seq, ctx); - ADD_HOOK_FUNCTION(ledger_last_hash, ctx); - ADD_HOOK_FUNCTION(ledger_last_time, ctx); - ADD_HOOK_FUNCTION(ledger_nonce, ctx); - ADD_HOOK_FUNCTION(ledger_keylet, ctx); - - ADD_HOOK_FUNCTION(hook_param, ctx); - ADD_HOOK_FUNCTION(hook_param_set, ctx); - ADD_HOOK_FUNCTION(hook_skip, ctx); - ADD_HOOK_FUNCTION(hook_pos, ctx); - - ADD_HOOK_FUNCTION(state, ctx); - ADD_HOOK_FUNCTION(state_foreign, ctx); - ADD_HOOK_FUNCTION(state_set, ctx); - ADD_HOOK_FUNCTION(state_foreign_set, ctx); - - ADD_HOOK_FUNCTION(slot, ctx); - ADD_HOOK_FUNCTION(slot_clear, ctx); - ADD_HOOK_FUNCTION(slot_count, ctx); - ADD_HOOK_FUNCTION(slot_set, ctx); - ADD_HOOK_FUNCTION(slot_size, ctx); - ADD_HOOK_FUNCTION(slot_subarray, ctx); - ADD_HOOK_FUNCTION(slot_subfield, ctx); - ADD_HOOK_FUNCTION(slot_type, ctx); - ADD_HOOK_FUNCTION(slot_float, ctx); - - ADD_HOOK_FUNCTION(trace, ctx); - ADD_HOOK_FUNCTION(trace_num, ctx); - ADD_HOOK_FUNCTION(trace_float, ctx); - - ADD_HOOK_FUNCTION(meta_slot, ctx); - ADD_HOOK_FUNCTION(xpop_slot, ctx); - - /* - ADD_HOOK_FUNCTION(str_find, ctx); - ADD_HOOK_FUNCTION(str_replace, ctx); - ADD_HOOK_FUNCTION(str_compare, ctx); - ADD_HOOK_FUNCTION(str_concat, ctx); - */ +#undef HOOK_API_DEFINITION +#undef HOOK_WRAP_PARAMS +#pragma pop_macro("HOOK_API_DEFINITION") WasmEdge_TableInstanceContext* hostTable = WasmEdge_TableInstanceCreate(tableType); diff --git a/src/ripple/app/hook/hook_api.macro b/src/ripple/app/hook/hook_api.macro new file mode 100644 index 000000000..6f76f4f8d --- /dev/null +++ b/src/ripple/app/hook/hook_api.macro @@ -0,0 +1,369 @@ +// int32_t _g(uint32_t guard_id, uint32_t maxiter); +HOOK_API_DEFINITION( + int32_t, _g, (uint32_t, uint32_t), + uint256{}) + +// int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code); +HOOK_API_DEFINITION( + int64_t, accept, (uint32_t, uint32_t, int64_t), + uint256{}) + +// int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code); +HOOK_API_DEFINITION( + int64_t, rollback, (uint32_t, uint32_t, int64_t), + uint256{}) + +// int64_t util_raddr(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, util_raddr, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t util_accid(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, util_accid, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t util_verify(uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len); +HOOK_API_DEFINITION( + int64_t, util_verify, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t util_sha512h(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, util_sha512h, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t util_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f); +HOOK_API_DEFINITION( + int64_t, util_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t sto_validate(uint32_t tread_ptr, uint32_t tread_len); +HOOK_API_DEFINITION( + int64_t, sto_validate, (uint32_t, uint32_t), + uint256{}) + +// int64_t sto_subfield(uint32_t read_ptr, uint32_t read_len, uint32_t field_id); +HOOK_API_DEFINITION( + int64_t, sto_subfield, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t sto_subarray(uint32_t read_ptr, uint32_t read_len, uint32_t array_id); +HOOK_API_DEFINITION( + int64_t, sto_subarray, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t sto_emplace(uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id); +HOOK_API_DEFINITION( + int64_t, sto_emplace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t sto_erase(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id); +HOOK_API_DEFINITION( + int64_t, sto_erase, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t etxn_burden(); +HOOK_API_DEFINITION( + int64_t, etxn_burden, (), + uint256{}) + +// int64_t etxn_details(uint32_t write_ptr, uint32_t write_len); +HOOK_API_DEFINITION( + int64_t, etxn_details, (uint32_t, uint32_t), + uint256{}) + +// int64_t etxn_fee_base(uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, etxn_fee_base, (uint32_t, uint32_t), + uint256{}) + +// int64_t etxn_reserve(uint32_t count); +HOOK_API_DEFINITION( + int64_t, etxn_reserve, (uint32_t), + uint256{}) + +// int64_t etxn_generation(); +HOOK_API_DEFINITION( + int64_t, etxn_generation, (), + uint256{}) + +// int64_t etxn_nonce(uint32_t write_ptr, uint32_t write_len); +HOOK_API_DEFINITION( + int64_t, etxn_nonce, (uint32_t, uint32_t), + uint256{}) + +// int64_t emit(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, emit, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t float_set(int32_t exponent, int64_t mantissa); +HOOK_API_DEFINITION( + int64_t, float_set, (int32_t, int64_t), + uint256{}) + +// int64_t float_multiply(int64_t float1, int64_t float2); +HOOK_API_DEFINITION( + int64_t, float_multiply, (int64_t, int64_t), + uint256{}) + +// int64_t float_mulratio(int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator); +HOOK_API_DEFINITION( + int64_t, float_mulratio, (int64_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t float_negate(int64_t float1); +HOOK_API_DEFINITION( + int64_t, float_negate, (int64_t), + uint256{}) + +// int64_t float_compare(int64_t float1, int64_t float2, uint32_t mode); +HOOK_API_DEFINITION( + int64_t, float_compare, (int64_t, int64_t, uint32_t), + uint256{}) + +// int64_t float_sum(int64_t float1, int64_t float2); +HOOK_API_DEFINITION( + int64_t, float_sum, (int64_t, int64_t), + uint256{}) + +// int64_t float_sto(uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code); +HOOK_API_DEFINITION( + int64_t, float_sto, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, int64_t, uint32_t), + uint256{}) + +// int64_t float_sto_set(uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, float_sto_set, (uint32_t, uint32_t), + uint256{}) + +// int64_t float_invert(int64_t float1); +HOOK_API_DEFINITION( + int64_t, float_invert, (int64_t), + uint256{}) + +// int64_t float_divide(int64_t float1, int64_t float2); +HOOK_API_DEFINITION( + int64_t, float_divide, (int64_t, int64_t), + uint256{}) + +// int64_t float_one(); +HOOK_API_DEFINITION( + int64_t, float_one, (), + uint256{}) + +// int64_t float_mantissa(int64_t float1); +HOOK_API_DEFINITION( + int64_t, float_mantissa, (int64_t), + uint256{}) + +// int64_t float_sign(int64_t float1); +HOOK_API_DEFINITION( + int64_t, float_sign, (int64_t), + uint256{}) + +// int64_t float_int(int64_t float1, uint32_t decimal_places, uint32_t abs); +HOOK_API_DEFINITION( + int64_t, float_int, (int64_t, uint32_t, uint32_t), + uint256{}) + +// int64_t float_log(int64_t float1); +HOOK_API_DEFINITION( + int64_t, float_log, (int64_t), + uint256{}) + +// int64_t float_root(int64_t float1, uint32_t n); +HOOK_API_DEFINITION( + int64_t, float_root, (int64_t, uint32_t), + uint256{}) + +// int64_t fee_base(); +HOOK_API_DEFINITION( + int64_t, fee_base, (), + uint256{}) + +// int64_t ledger_seq(); +HOOK_API_DEFINITION( + int64_t, ledger_seq, (), + uint256{}) + +// int64_t ledger_last_time(); +HOOK_API_DEFINITION( + int64_t, ledger_last_time, (), + uint256{}) + +// int64_t ledger_last_hash(uint32_t write_ptr, uint32_t write_len); +HOOK_API_DEFINITION( + int64_t, ledger_last_hash, (uint32_t, uint32_t), + uint256{}) + +// int64_t ledger_nonce(uint32_t write_ptr, uint32_t write_len); +HOOK_API_DEFINITION( + int64_t, ledger_nonce, (uint32_t, uint32_t), + uint256{}) + +// int64_t ledger_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len); +HOOK_API_DEFINITION( + int64_t, ledger_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t hook_account(uint32_t write_ptr, uint32_t write_len); +HOOK_API_DEFINITION( + int64_t, hook_account, (uint32_t, uint32_t), + uint256{}) + +// int64_t hook_hash(uint32_t write_ptr, uint32_t write_len, int32_t hook_no); +HOOK_API_DEFINITION( + int64_t, hook_hash, (uint32_t, uint32_t, int32_t), + uint256{}) + +// int64_t hook_param_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len); +HOOK_API_DEFINITION( + int64_t, hook_param_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t hook_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, hook_param, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t hook_again(); +HOOK_API_DEFINITION( + int64_t, hook_again, (), + uint256{}) + +// int64_t hook_skip(uint32_t read_ptr, uint32_t read_len, uint32_t flags); +HOOK_API_DEFINITION( + int64_t, hook_skip, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t hook_pos(); +HOOK_API_DEFINITION( + int64_t, hook_pos, (), + uint256{}) + +// int64_t slot(uint32_t write_ptr, uint32_t write_len, uint32_t slot); +HOOK_API_DEFINITION( + int64_t, slot, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t slot_clear(uint32_t slot); +HOOK_API_DEFINITION( + int64_t, slot_clear, (uint32_t), + uint256{}) + +// int64_t slot_count(uint32_t slot); +HOOK_API_DEFINITION( + int64_t, slot_count, (uint32_t), + uint256{}) + +// int64_t slot_set(uint32_t read_ptr, uint32_t read_len, uint32_t slot); +HOOK_API_DEFINITION( + int64_t, slot_set, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t slot_size(uint32_t slot); +HOOK_API_DEFINITION( + int64_t, slot_size, (uint32_t), + uint256{}) + +// int64_t slot_subarray(uint32_t parent_slot, uint32_t array_id, uint32_t new_slot); +HOOK_API_DEFINITION( + int64_t, slot_subarray, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t slot_subfield(uint32_t parent_slot, uint32_t field_id, uint32_t new_slot); +HOOK_API_DEFINITION( + int64_t, slot_subfield, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t slot_type(uint32_t slot_no, uint32_t flags); +HOOK_API_DEFINITION( + int64_t, slot_type, (uint32_t, uint32_t), + uint256{}) + +// int64_t slot_float(uint32_t slot_no); +HOOK_API_DEFINITION( + int64_t, slot_float, (uint32_t), + uint256{}) + +// int64_t state_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len); +HOOK_API_DEFINITION( + int64_t, state_set, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t state_foreign_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len); +HOOK_API_DEFINITION( + int64_t, state_foreign_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t state(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len); +HOOK_API_DEFINITION( + int64_t, state, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t state_foreign(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len); +HOOK_API_DEFINITION( + int64_t, state_foreign, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t trace(uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex); +HOOK_API_DEFINITION( + int64_t, trace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t trace_num(uint32_t read_ptr, uint32_t read_len, int64_t number); +HOOK_API_DEFINITION( + int64_t, trace_num, (uint32_t, uint32_t, int64_t), + uint256{}) + +// int64_t trace_float(uint32_t read_ptr, uint32_t read_len, int64_t float1); +HOOK_API_DEFINITION( + int64_t, trace_float, (uint32_t, uint32_t, int64_t), + uint256{}) + +// int64_t otxn_burden(); +HOOK_API_DEFINITION( + int64_t, otxn_burden, (), + uint256{}) + +// int64_t otxn_field(uint32_t write_ptr, uint32_t write_len, uint32_t field_id); +HOOK_API_DEFINITION( + int64_t, otxn_field, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t otxn_generation(); +HOOK_API_DEFINITION( + int64_t, otxn_generation, (), + uint256{}) + +// int64_t otxn_id(uint32_t write_ptr, uint32_t write_len, uint32_t flags); +HOOK_API_DEFINITION( + int64_t, otxn_id, (uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t otxn_type(); +HOOK_API_DEFINITION( + int64_t, otxn_type, (), + uint256{}) + +// int64_t otxn_slot(uint32_t slot_no); +HOOK_API_DEFINITION( + int64_t, otxn_slot, (uint32_t), + uint256{}) + +// int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len); +HOOK_API_DEFINITION( + int64_t, otxn_param, (uint32_t, uint32_t, uint32_t, uint32_t), + uint256{}) + +// int64_t meta_slot(uint32_t slot_no); +HOOK_API_DEFINITION( + int64_t, meta_slot, (uint32_t), + uint256{}) + +// int64_t xpop_slot(uint32_t slot_no_tx, uint32_t slot_no_meta); +HOOK_API_DEFINITION( + int64_t, xpop_slot, (uint32_t, uint32_t), + featureHooksUpdate1) diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index f27b7d23b..50da50102 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -2275,7 +2275,7 @@ DEFINE_HOOK_FUNCTION( } // Return the tt (Transaction Type) numeric code of the originating transaction -DEFINE_HOOK_FUNCNARG(int64_t, otxn_type) +DEFINE_HOOK_FUNCTION(int64_t, otxn_type) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack @@ -2325,7 +2325,7 @@ DEFINE_HOOK_FUNCTION(int64_t, otxn_slot, uint32_t slot_into) // Return the burden of the originating transaction... this will be 1 unless the // originating transaction was itself an emitted transaction from a previous // hook invocation -DEFINE_HOOK_FUNCNARG(int64_t, otxn_burden) +DEFINE_HOOK_FUNCTION(int64_t, otxn_burden) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack @@ -2362,7 +2362,7 @@ DEFINE_HOOK_FUNCNARG(int64_t, otxn_burden) // Return the generation of the originating transaction... this will be 1 unless // the originating transaction was itself an emitted transaction from a previous // hook invocation -DEFINE_HOOK_FUNCNARG(int64_t, otxn_generation) +DEFINE_HOOK_FUNCTION(int64_t, otxn_generation) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack @@ -2394,14 +2394,14 @@ DEFINE_HOOK_FUNCNARG(int64_t, otxn_generation) } // Return the generation of a hypothetically emitted transaction from this hook -DEFINE_HOOK_FUNCNARG(int64_t, etxn_generation) +DEFINE_HOOK_FUNCTION(int64_t, etxn_generation) { // proxy only, no setup or teardown return otxn_generation(hookCtx, frameCtx) + 1; } // Return the current ledger sequence number -DEFINE_HOOK_FUNCNARG(int64_t, ledger_seq) +DEFINE_HOOK_FUNCTION(int64_t, ledger_seq) { HOOK_SETUP(); @@ -2431,7 +2431,7 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } -DEFINE_HOOK_FUNCNARG(int64_t, ledger_last_time) +DEFINE_HOOK_FUNCTION(int64_t, ledger_last_time) { HOOK_SETUP(); @@ -3889,7 +3889,7 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count) } // Compute the burden of an emitted transaction based on a number of factors -DEFINE_HOOK_FUNCNARG(int64_t, etxn_burden) +DEFINE_HOOK_FUNCTION(int64_t, etxn_burden) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack @@ -4711,7 +4711,7 @@ DEFINE_HOOK_FUNCTION( } // Return the current fee base of the current ledger (multiplied by a margin) -DEFINE_HOOK_FUNCNARG(int64_t, fee_base) +DEFINE_HOOK_FUNCTION(int64_t, fee_base) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack @@ -5634,7 +5634,7 @@ DEFINE_HOOK_FUNCTION(int64_t, float_divide, int64_t float1, int64_t float2) HOOK_TEARDOWN(); } -DEFINE_HOOK_FUNCNARG(int64_t, float_one) +DEFINE_HOOK_FUNCTION(int64_t, float_one) { return float_one_internal; } @@ -6030,12 +6030,12 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } -DEFINE_HOOK_FUNCNARG(int64_t, hook_pos) +DEFINE_HOOK_FUNCTION(int64_t, hook_pos) { return hookCtx.result.hookChainPosition; } -DEFINE_HOOK_FUNCNARG(int64_t, hook_again) +DEFINE_HOOK_FUNCTION(int64_t, hook_again) { HOOK_SETUP(); From d20927237963a92127780d280b550b17e47e2ece Mon Sep 17 00:00:00 2001 From: tequ Date: Sun, 15 Feb 2026 19:45:23 +0900 Subject: [PATCH 3/5] Hook API Refactoring / Unit Testing (#581) --- Builds/CMake/RippledCore.cmake | 2 + src/ripple/app/hook/HookAPI.h | 346 ++ src/ripple/app/hook/Macro.h | 1 + src/ripple/app/hook/applyHook.h | 14 +- src/ripple/app/hook/impl/HookAPI.cpp | 3197 +++++++++++++++++ src/ripple/app/hook/impl/applyHook.cpp | 2910 ++------------- src/ripple/basics/Expected.h | 6 +- src/test/app/HookAPI_test.cpp | 4543 ++++++++++++++++++++++++ src/test/app/SetHook_test.cpp | 42 + src/test/app/SetHook_wasm.h | 315 +- src/test/jtx/hook.h | 73 + src/test/jtx/impl/hook.cpp | 77 + 12 files changed, 8820 insertions(+), 2706 deletions(-) create mode 100644 src/ripple/app/hook/HookAPI.h create mode 100644 src/ripple/app/hook/impl/HookAPI.cpp create mode 100644 src/test/app/HookAPI_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 74ff58f60..0ae80cea0 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -488,6 +488,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/apply.cpp src/ripple/app/tx/impl/applySteps.cpp src/ripple/app/hook/impl/applyHook.cpp + src/ripple/app/hook/impl/HookAPI.cpp src/ripple/app/tx/impl/details/NFTokenUtils.cpp #[===============================[ main sources: @@ -749,6 +750,7 @@ if (tests) src/test/app/Freeze_test.cpp src/test/app/GenesisMint_test.cpp src/test/app/HashRouter_test.cpp + src/test/app/HookAPI_test.cpp src/test/app/Import_test.cpp src/test/app/Invoke_test.cpp src/test/app/LedgerHistory_test.cpp diff --git a/src/ripple/app/hook/HookAPI.h b/src/ripple/app/hook/HookAPI.h new file mode 100644 index 000000000..a8411789d --- /dev/null +++ b/src/ripple/app/hook/HookAPI.h @@ -0,0 +1,346 @@ +#ifndef HOOK_API_INCLUDED +#define HOOK_API_INCLUDED 1 + +#include +#include + +namespace hook { +using namespace ripple; +using HookReturnCode = hook_api::hook_return_code; + +using Bytes = std::vector; + +struct HookContext; // defined in applyHook.h + +class HookAPI +{ +public: + explicit HookAPI(HookContext& ctx) : hookCtx(ctx) + { + } + + /// control APIs + // _g + // accept + // rollback + + /// util APIs + Expected + util_raddr(Bytes const& accountID) const; + + Expected + util_accid(std::string raddress) const; + + Expected + util_verify(Slice const& data, Slice const& sig, Slice const& key) const; + + uint256 + util_sha512h(Slice const& data) const; + + // util_keylet() + + /// sto APIs + Expected + sto_validate(Bytes const& data) const; + + Expected, HookReturnCode> + sto_subfield(Bytes const& data, uint32_t field_id) const; + + Expected, HookReturnCode> + sto_subarray(Bytes const& data, uint32_t index_id) const; + + Expected + sto_emplace( + Bytes const& source_object, + std::optional const& field_object, + uint32_t field_id) const; + + // sto_erase(): same as sto_emplace with field_object = nullopt + + /// etxn APIs + Expected, HookReturnCode> + emit(Slice const& txBlob) const; + + Expected + etxn_burden() const; + + Expected + etxn_fee_base(Slice const& txBlob) const; + + Expected + etxn_details(uint8_t* out_ptr) const; + + Expected + etxn_reserve(uint64_t count) const; + + uint32_t + etxn_generation() const; + + Expected + etxn_nonce() const; + + /// float APIs + Expected + float_set(int32_t exponent, int64_t mantissa) const; + + Expected + float_multiply(uint64_t float1, uint64_t float2) const; + + Expected + float_mulratio( + uint64_t float1, + uint32_t round_up, + uint32_t numerator, + uint32_t denominator) const; + + uint64_t + float_negate(uint64_t float1) const; + + Expected + float_compare(uint64_t float1, uint64_t float2, uint32_t mode) const; + + Expected + float_sum(uint64_t float1, uint64_t float2) const; + + Expected + float_sto( + std::optional currency, + std::optional issuer, + uint64_t float1, + uint32_t field_code, + uint32_t write_len) const; + + Expected + float_sto_set(Bytes const& data) const; + + Expected + float_invert(uint64_t float1) const; + + Expected + float_divide(uint64_t float1, uint64_t float2) const; + + uint64_t + float_one() const; + + Expected + float_mantissa(uint64_t float1) const; + + uint64_t + float_sign(uint64_t float1) const; + + Expected + float_int(uint64_t float1, uint32_t decimal_places, uint32_t absolute) + const; + + Expected + float_log(uint64_t float1) const; + + Expected + float_root(uint64_t float1, uint32_t n) const; + + /// otxn APIs + uint64_t + otxn_burden() const; + + uint32_t + otxn_generation() const; + + Expected + otxn_field(uint32_t field_id) const; + + Expected + otxn_id(uint32_t flags) const; + + TxType + otxn_type() const; + + Expected + otxn_slot(uint32_t slot_into) const; + + Expected + otxn_param(Bytes const& param_name) const; + + /// hook APIs + AccountID + hook_account() const; + + Expected + hook_hash(int32_t hook_no) const; + + Expected + hook_again() const; + + Expected + hook_param(Bytes const& paramName) const; + + Expected + hook_param_set( + uint256 const& hash, + Bytes const& paramName, + Bytes const& paramValue) const; + + Expected + hook_skip(uint256 const& hash, uint32_t flags) const; + + uint8_t + hook_pos() const; + + /// ledger APIs + uint64_t + fee_base() const; + + uint32_t + ledger_seq() const; + + uint256 + ledger_last_hash() const; + + uint64_t + ledger_last_time() const; + + Expected + ledger_nonce() const; + + Expected + ledger_keylet(Keylet const& klLo, Keylet const& klHi) const; + + /// state APIs + + // state(): same as state_foreign with ns = 0 and account = hook_account() + + Expected + state_foreign( + uint256 const& key, + uint256 const& ns, + AccountID const& account) const; + + // state_set(): same as state_foreign_set with ns = 0 and account = + + Expected + state_foreign_set( + uint256 const& key, + uint256 const& ns, + AccountID const& account, + Bytes& data) const; + + /// slot APIs + Expected + slot(uint32_t slot_no) const; + + Expected + slot_clear(uint32_t slot_no) const; + + Expected + slot_count(uint32_t slot_no) const; + + Expected + slot_set(Bytes const& data, uint32_t slot_no) const; + + Expected + slot_size(uint32_t slot_no) const; + + Expected + slot_subarray(uint32_t parent_slot, uint32_t array_id, uint32_t new_slot) + const; + + Expected + slot_subfield(uint32_t parent_slot, uint32_t field_id, uint32_t new_slot) + const; + + Expected, HookReturnCode> + slot_type(uint32_t slot_no, uint32_t flags) const; + + Expected + slot_float(uint32_t slot_no) const; + + /// trace APIs + // trace + // trace_num + // trace_float + + Expected + meta_slot(uint32_t slot_into) const; + + Expected, HookReturnCode> + xpop_slot(uint32_t slot_into_tx, uint32_t slot_into_meta) const; + +private: + HookContext& hookCtx; + + inline int32_t + no_free_slots() const; + + inline std::optional + get_free_slot() const; + + inline Expected + float_multiply_internal_parts( + uint64_t man1, + int32_t exp1, + bool neg1, + uint64_t man2, + int32_t exp2, + bool neg2) const; + + inline Expected + mulratio_internal( + int64_t& man1, + int32_t& exp1, + bool round_up, + uint32_t numerator, + uint32_t denominator) const; + + inline Expected + float_divide_internal(uint64_t float1, uint64_t float2) const; + + inline Expected + double_to_xfl(double x) const; + + std::optional + unserialize_keylet(Bytes const& data) const; + + // update the state cache + inline std::optional< + std::reference_wrapper const>> + lookup_state_cache( + AccountID const& acc, + uint256 const& ns, + uint256 const& key) const; + + // check the state cache + inline Expected + set_state_cache( + AccountID const& acc, + uint256 const& ns, + uint256 const& key, + Bytes const& data, + bool modified) const; + + // these are only used by get_stobject_length below + enum parse_error { + pe_unexpected_end = -1, + pe_unknown_type_early = -2, // detected early + pe_unknown_type_late = -3, // end of function + pe_excessive_nesting = -4, + pe_excessive_size = -5 + }; + + inline Expected< + int32_t, + parse_error> + get_stobject_length( + unsigned char* start, // in - begin iterator + unsigned char* maxptr, // in - end iterator + int& type, // out - populated by serialized type code + int& field, // out - populated by serialized field code + int& payload_start, // out - the start of actual payload data for + // this type + int& payload_length, // out - the length of actual payload data for + // this type + int recursion_depth = 0) // used internally + const; +}; + +} // namespace hook + +#endif // HOOK_API_INCLUDED diff --git a/src/ripple/app/hook/Macro.h b/src/ripple/app/hook/Macro.h index ebf3499b2..99d1f42f7 100644 --- a/src/ripple/app/hook/Macro.h +++ b/src/ripple/app/hook/Macro.h @@ -153,6 +153,7 @@ [[maybe_unused]] const uint64_t memory_length = \ WasmEdge_MemoryInstanceGetPageSize(memoryCtx) * \ WasmEdge_kPageSize; \ + [[maybe_unused]] auto& api = hookCtx.api(); \ if (!memoryCtx || !memory || !memory_length) \ return INTERNAL_ERROR; diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 3fa02cbe6..d12bd8038 100644 --- a/src/ripple/app/hook/applyHook.h +++ b/src/ripple/app/hook/applyHook.h @@ -1,6 +1,7 @@ #ifndef APPLY_HOOK_INCLUDED #define APPLY_HOOK_INCLUDED 1 #include +#include #include #include #include @@ -132,7 +133,6 @@ struct HookResult ripple::uint256 const hookHash; ripple::uint256 const hookCanEmit; ripple::Keylet const accountKeylet; - ripple::Keylet const ownerDirKeylet; ripple::Keylet const hookKeylet; ripple::AccountID const account; ripple::AccountID const otxnAccount; @@ -209,6 +209,18 @@ struct HookContext // emitted txn then this optional becomes // populated with the SLE const HookExecutor* module = 0; + + // Lazy-initialized HookAPI member + mutable std::unique_ptr api_; + + // Access the HookAPI instance (lazy initialization) + HookAPI& + api() const + { + if (!api_) + api_ = std::make_unique(const_cast(*this)); + return *api_; + } }; bool diff --git a/src/ripple/app/hook/impl/HookAPI.cpp b/src/ripple/app/hook/impl/HookAPI.cpp new file mode 100644 index 000000000..8f2ff36ff --- /dev/null +++ b/src/ripple/app/hook/impl/HookAPI.cpp @@ -0,0 +1,3197 @@ +// Implementation of decoupled Hook APIs for emit and related helpers. + +#include +#include +#include +#include +#include + +namespace hook { +namespace hook_float { + +// power of 10 LUT for fast integer math +static int64_t power_of_ten[19] = { + 1LL, + 10LL, + 100LL, + 1000LL, + 10000LL, + 100000LL, + 1000000LL, + 10000000LL, + 100000000LL, + 1000000000LL, + 10000000000LL, + 100000000000LL, + 1000000000000LL, + 10000000000000LL, + 100000000000000LL, + 1000000000000000LL, // 15 + 10000000000000000LL, + 100000000000000000LL, + 1000000000000000000LL, +}; + +using namespace hook_api; +static int64_t const minMantissa = 1000000000000000ull; +static int64_t const maxMantissa = 9999999999999999ull; +static int32_t const minExponent = -96; +static int32_t const maxExponent = 80; + +inline Expected +get_exponent(int64_t float1) +{ + if (float1 < 0) + return Unexpected(INVALID_FLOAT); + if (float1 == 0) + return 0; + uint64_t float_in = (uint64_t)float1; + float_in >>= 54U; + float_in &= 0xFFU; + return ((int32_t)float_in) - 97; +} + +inline Expected +get_mantissa(int64_t float1) +{ + if (float1 < 0) + return Unexpected(INVALID_FLOAT); + if (float1 == 0) + return 0; + float1 -= ((((uint64_t)float1) >> 54U) << 54U); + return float1; +} + +inline bool +is_negative(int64_t float1) +{ + return ((float1 >> 62U) & 1ULL) == 0; +} + +inline int64_t +invert_sign(int64_t float1) +{ + int64_t r = (int64_t)(((uint64_t)float1) ^ (1ULL << 62U)); + return r; +} + +inline int64_t +set_sign(int64_t float1, bool set_negative) +{ + bool neg = is_negative(float1); + if ((neg && set_negative) || (!neg && !set_negative)) + return float1; + + return invert_sign(float1); +} + +inline Expected +set_mantissa(int64_t float1, uint64_t mantissa) +{ + if (mantissa > maxMantissa) + return Unexpected(MANTISSA_OVERSIZED); + if (mantissa < minMantissa) + return Unexpected(MANTISSA_UNDERSIZED); + return float1 - get_mantissa(float1).value() + mantissa; +} + +inline Expected +set_exponent(int64_t float1, int32_t exponent) +{ + if (exponent > maxExponent) + return Unexpected(EXPONENT_OVERSIZED); + if (exponent < minExponent) + return Unexpected(EXPONENT_UNDERSIZED); + + uint64_t exp = (exponent + 97); + exp <<= 54U; + float1 &= ~(0xFFLL << 54); + float1 += (int64_t)exp; + return float1; +} + +inline Expected +make_float(ripple::IOUAmount& amt) +{ + int64_t man_out = amt.mantissa(); + int64_t float_out = 0; + bool neg = man_out < 0; + if (neg) + man_out *= -1; + + float_out = set_sign(float_out, neg); + auto const mantissa = set_mantissa(float_out, (uint64_t)man_out); + if (!mantissa) + // TODO: This change requires the amendment. + // return Unexpected(mantissa.error()); + float_out = mantissa.error(); + else + float_out = mantissa.value(); + auto const exponent = set_exponent(float_out, amt.exponent()); + if (!exponent) + return Unexpected(exponent.error()); + float_out = exponent.value(); + return float_out; +} + +inline Expected +make_float(uint64_t mantissa, int32_t exponent, bool neg) +{ + if (mantissa == 0) + return 0; + if (mantissa > maxMantissa) + return Unexpected(MANTISSA_OVERSIZED); + if (mantissa < minMantissa) + return Unexpected(MANTISSA_UNDERSIZED); + if (exponent > maxExponent) + return Unexpected(EXPONENT_OVERSIZED); + if (exponent < minExponent) + return Unexpected(EXPONENT_UNDERSIZED); + int64_t out = 0; + + auto const m = set_mantissa(out, mantissa); + if (!m) + return m.error(); + out = m.value(); + + auto const e = set_exponent(out, exponent); + if (!e) + return e.error(); + out = e.value(); + + out = set_sign(out, neg); + return out; +} + +/** + * This function normalizes the mantissa and exponent passed, if it can. + * It returns the XFL and mutates the supplied manitssa and exponent. + * If a negative mantissa is provided then the returned XFL has the negative + * flag set. If there is an overflow error return XFL_OVERFLOW. On underflow + * returns canonical 0 + */ +template +inline Expected +normalize_xfl(T& man, int32_t& exp, bool neg = false) +{ + if (man == 0) + return 0; + + if (man == std::numeric_limits::min()) + man++; + + constexpr bool sman = std::is_same::value; + static_assert(sman || std::is_same()); + + if constexpr (sman) + { + if (man < 0) + { + man *= -1LL; + neg = true; + } + } + + // mantissa order + std::feclearexcept(FE_ALL_EXCEPT); + int32_t mo = log10(man); + // defensively ensure log10 produces a sane result; we'll borrow the + // overflow error code if it didn't + if (std::fetestexcept(FE_INVALID)) + return Unexpected(XFL_OVERFLOW); + + int32_t adjust = 15 - mo; + + if (adjust > 0) + { + // defensive check + if (adjust > 18) + return 0; + man *= power_of_ten[adjust]; + exp -= adjust; + } + else if (adjust < 0) + { + // defensive check + if (-adjust > 18) + return Unexpected(XFL_OVERFLOW); + man /= power_of_ten[-adjust]; + exp -= adjust; + } + + if (man == 0) + { + exp = 0; + return 0; + } + + // even after adjustment the mantissa can be outside the range by one place + // improving the math above would probably alleviate the need for these + // branches + if (man < minMantissa) + { + if (man == minMantissa - 1LL) + man += 1LL; + else + { + man *= 10LL; + exp--; + } + } + + if (man > maxMantissa) + { + if (man == maxMantissa + 1LL) + man -= 1LL; + else + { + man /= 10LL; + exp++; + } + } + + if (exp < minExponent) + { + man = 0; + exp = 0; + return 0; + } + + if (man == 0) + { + exp = 0; + return 0; + } + + if (exp > maxExponent) + return Unexpected(XFL_OVERFLOW); + + auto const ret = make_float((uint64_t)man, exp, neg); + if constexpr (sman) + { + if (neg) + man *= -1LL; + } + + if (!ret) + return ret.error(); + + return ret; +} + +const int64_t float_one_internal = + make_float(1000000000000000ull, -15, false).value(); + +} // namespace hook_float + +using namespace ripple; +using namespace hook_float; + +/// control APIs +// _g +// accept +// rollback + +/// util APIs +Expected +HookAPI::util_raddr(Bytes const& accountID) const +{ + if (accountID.size() != 20) + return Unexpected(INVALID_ARGUMENT); + + return encodeBase58Token( + TokenType::AccountID, accountID.data(), accountID.size()); +} + +Expected +HookAPI::util_accid(std::string raddress) const +{ + auto const result = decodeBase58Token(raddress, TokenType::AccountID); + if (result.empty()) + return Unexpected(INVALID_ARGUMENT); + return Bytes(result.data(), result.data() + result.size()); +} + +Expected +HookAPI::util_verify(Slice const& data, Slice const& sig, Slice const& key) + const +{ + if (key.size() != 33) + return Unexpected(INVALID_KEY); + + if (data.size() == 0) + return Unexpected(TOO_SMALL); + + if (sig.size() < 30) + return Unexpected(TOO_SMALL); + + if (!publicKeyType(key)) + return Unexpected(INVALID_KEY); + + ripple::PublicKey pubkey{key}; + return ripple::verify(pubkey, data, sig, false); +} + +uint256 +HookAPI::util_sha512h(Slice const& data) const +{ + return ripple::sha512Half(data); +} + +// util_keylet + +/// sto APIs +Expected +HookAPI::sto_validate(Bytes const& data) const +{ + if (data.size() < 2) + return Unexpected(TOO_SMALL); + + unsigned char* start = const_cast(data.data()); + unsigned char* upto = start; + unsigned char* end = start + data.size(); + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + auto const length = get_stobject_length( + upto, end, type, field, payload_start, payload_length, 0); + if (!length) + return 0; + upto += length.value(); + } + + return upto == end ? 1 : 0; +} + +Expected, HookReturnCode> +HookAPI::sto_subfield(Bytes const& data, uint32_t field_id) const +{ + if (data.size() < 2) + return Unexpected(TOO_SMALL); + + unsigned char* start = const_cast(data.data()); + unsigned char* upto = start; + unsigned char* end = start + data.size(); + + DBG_PRINTF( + "sto_subfield called, looking for field %u type %u\n", + field_id & 0xFFFF, + (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF((j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + + // if ((*upto & 0xF0) == 0xE0) + // upto++; + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + auto const length = get_stobject_length( + upto, end, type, field, payload_start, payload_length, 0); + if (!length) + return Unexpected(PARSE_ERROR); + if ((type << 16) + field == field_id) + { + DBG_PRINTF( + "sto_subfield returned for field %u type %u\n", + field_id & 0xFFFF, + (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF((j == 0 ? " [%02X] " : " %02X "), *(upto + j)); + DBG_PRINTF("\n"); + + if (type == 0xF) // we return arrays fully formed + return std::make_pair(upto - start, length.value()); + + // return pointers to all other objects as payloads + return std::make_pair(upto - start + payload_start, payload_length); + } + upto += length.value(); + } + + if (upto != end) + return Unexpected(PARSE_ERROR); + + return Unexpected(DOESNT_EXIST); +} + +Expected, HookReturnCode> +HookAPI::sto_subarray(Bytes const& data, uint32_t index_id) const +{ + if (data.size() < 2) + return Unexpected(TOO_SMALL); + + unsigned char* start = const_cast(data.data()); + unsigned char* upto = start; + unsigned char* end = start + data.size(); + + // unwrap the array if it is wrapped, + // by removing a byte from the start and end + // why here 0xF0? + // STI_ARRAY = 0xF0 + // eg) Signers field value = 0x03 => 0xF3 + // eg) Amounts field value = 0x5C => 0xF0, 0x5C + if ((*upto & 0xF0U) == 0xF0U) + { + if (hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128) && + *upto == 0xF0U) + { + // field value > 15 + upto++; + upto++; + end--; + } + else + { + // field value <= 15 + upto++; + end--; + } + } + + if (upto >= end) + return Unexpected(PARSE_ERROR); + + /* + DBG_PRINTF("sto_subarray called, looking for index %u\n", index_id); + for (int j = -5; j < 5; ++j) + printf(( j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + */ + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + auto const length = get_stobject_length( + upto, end, type, field, payload_start, payload_length, 0); + if (!length) + return Unexpected(PARSE_ERROR); + + if (i == index_id) + { + DBG_PRINTF("sto_subarray returned for index %u\n", index_id); + for (int j = -5; j < 5; ++j) + DBG_PRINTF( + (j == 0 ? " [%02X] " : " %02X "), + *(upto + j + length.value())); + DBG_PRINTF("\n"); + + return std::make_pair(upto - start, length.value()); + } + upto += length.value(); + } + + if (upto != end) + return Unexpected(PARSE_ERROR); + + return Unexpected(DOESNT_EXIST); +} + +Expected +HookAPI::sto_emplace( + Bytes const& source_object, + std::optional const& field_object, + uint32_t field_id) const +{ + // RH TODO: put these constants somewhere (votable?) + if (source_object.size() > 1024 * 16) + return Unexpected(TOO_BIG); + + if (source_object.size() < 2) + return Unexpected(TOO_SMALL); + + if (!field_object.has_value()) + { + // this is a delete operation + } + else + { + if (field_object->size() > 4096) + return Unexpected(TOO_BIG); + + if (field_object->size() < 2) + return Unexpected(TOO_SMALL); + } + + if (field_object.has_value() && + hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128)) + { + // inject field should be valid sto object and it's field id should + // match the field_id + unsigned char* inject_start = (unsigned char*)(field_object->data()); + unsigned char* inject_end = + (unsigned char*)(field_object->data() + field_object->size()); + int type = -1, field = -1, payload_start = -1, payload_length = -1; + auto const length = get_stobject_length( + inject_start, + inject_end, + type, + field, + payload_start, + payload_length, + 0); + if (!length) + return Unexpected(PARSE_ERROR); + if ((type << 16) + field != field_id) + { + return Unexpected(PARSE_ERROR); + } + } + + std::vector out( + (size_t)( + source_object.size() + (field_object ? field_object->size() : 0)), + (uint8_t)0); + uint8_t* write_ptr = out.data(); + + // we must inject the field at the canonical location.... + // so find that location + unsigned char* start = (unsigned char*)(source_object.data()); + unsigned char* upto = start; + unsigned char* end = start + source_object.size(); + unsigned char* inject_start = end; + unsigned char* inject_end = end; + + DBG_PRINTF( + "sto_emplace called, looking for field %u type %u\n", + field_id & 0xFFFF, + (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF((j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + auto const length = get_stobject_length( + upto, end, type, field, payload_start, payload_length, 0); + if (!length) + return Unexpected(PARSE_ERROR); + if ((type << 16) + field == field_id) + { + inject_start = upto; + inject_end = upto + length.value(); + break; + } + else if ((type << 16) + field > field_id) + { + inject_start = upto; + inject_end = upto; + break; + } + upto += length.value(); + } + + // if the scan loop ends past the end of the source object + // then the source object is invalid/corrupt, so we must + // return an error + if (upto > end) + return Unexpected(PARSE_ERROR); + + // upto is injection point + int64_t bytes_written = 0; + + // part 1 + if (inject_start - start > 0) + { + size_t len = inject_start - start; + memcpy(write_ptr, start, len); + bytes_written += len; + } + + if (field_object && field_object->size() > 0) + { + // write the field (or don't if it's a delete operation) + memcpy( + write_ptr + bytes_written, + field_object->data(), + field_object->size()); + bytes_written += field_object->size(); + } + + // part 2 + if (end - inject_end > 0) + { + size_t len = end - inject_end; + memcpy(write_ptr + bytes_written, inject_end, len); + bytes_written += len; + } + + out.resize(bytes_written); + return out; +} + +// sto_erase + +/// etxn APIs +Expected, HookReturnCode> +HookAPI::emit(Slice const& txBlob) const +{ + auto& applyCtx = hookCtx.applyCtx; + auto j = applyCtx.app.journal("View"); + auto& view = applyCtx.view(); + + if (hookCtx.expected_etxn_count < 0) + return Unexpected(PREREQUISITE_NOT_MET); + + if (hookCtx.result.emittedTxn.size() >= hookCtx.expected_etxn_count) + return Unexpected(TOO_MANY_EMITTED_TXN); + + std::shared_ptr stpTrans; + try + { + SerialIter sit(txBlob); + stpTrans = std::make_shared(sit); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() << "]: Failed " << e.what(); + return Unexpected(EMISSION_FAILURE); + } + + if (isPseudoTx(*stpTrans)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: Attempted to emit pseudo txn."; + return Unexpected(EMISSION_FAILURE); + } + + ripple::TxType txType = stpTrans->getTxnType(); + + ripple::uint256 const& hookCanEmit = hookCtx.result.hookCanEmit; + if (!hook::canEmit(txType, hookCanEmit)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: Hook cannot emit this txn."; + return Unexpected(EMISSION_FAILURE); + } + + // check the emitted txn is valid + /* Emitted TXN rules + * 0. Account must match the hook account + * 1. Sequence: 0 + * 2. PubSigningKey: 000000000000000 + * 3. sfEmitDetails present and valid + * 4. No sfTxnSignature + * 5. LastLedgerSeq > current ledger, > firstledgerseq & LastLedgerSeq < seq + * + 5 + * 6. FirstLedgerSeq > current ledger + * 7. Fee must be correctly high + * 8. The generation cannot be higher than 10 + */ + + // rule 0: account must match the hook account + if (!stpTrans->isFieldPresent(sfAccount) || + stpTrans->getAccountID(sfAccount) != hookCtx.result.account) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfAccount does not match hook account"; + return Unexpected(EMISSION_FAILURE); + } + + // rule 1: sfSequence must be present and 0 + if (!stpTrans->isFieldPresent(sfSequence) || + stpTrans->getFieldU32(sfSequence) != 0) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfSequence missing or non-zero"; + return Unexpected(EMISSION_FAILURE); + } + + // rule 2: sfSigningPubKey must be present and 00...00 + if (!stpTrans->isFieldPresent(sfSigningPubKey)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfSigningPubKey missing"; + return Unexpected(EMISSION_FAILURE); + } + + auto const pk = stpTrans->getSigningPubKey(); + if (pk.size() != 33 && pk.size() != 0) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfSigningPubKey present but wrong size"; + return Unexpected(EMISSION_FAILURE); + } + + for (int i = 0; i < pk.size(); ++i) + if (pk[i] != 0) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfSigningPubKey present but non-zero."; + return Unexpected(EMISSION_FAILURE); + } + + // rule 2.a: no signers + if (stpTrans->isFieldPresent(sfSigners)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfSigners not allowed in emitted txns."; + return Unexpected(EMISSION_FAILURE); + } + + // rule 2.b: ticketseq cannot be used + if (stpTrans->isFieldPresent(sfTicketSequence)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfTicketSequence not allowed in emitted txns."; + return Unexpected(EMISSION_FAILURE); + } + + // rule 2.c sfAccountTxnID not allowed + if (stpTrans->isFieldPresent(sfAccountTxnID)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfAccountTxnID not allowed in emitted txns."; + return Unexpected(EMISSION_FAILURE); + } + + // rule 3: sfEmitDetails must be present and valid + if (!stpTrans->isFieldPresent(sfEmitDetails)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitDetails missing."; + return Unexpected(EMISSION_FAILURE); + } + + auto const& emitDetails = const_cast(*stpTrans) + .getField(sfEmitDetails) + .downcast(); + + if (!emitDetails.isFieldPresent(sfEmitGeneration) || + !emitDetails.isFieldPresent(sfEmitBurden) || + !emitDetails.isFieldPresent(sfEmitParentTxnID) || + !emitDetails.isFieldPresent(sfEmitNonce) || + !emitDetails.isFieldPresent(sfEmitHookHash)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitDetails malformed."; + return Unexpected(EMISSION_FAILURE); + } + + // rule 8: emit generation cannot exceed 10 + if (emitDetails.getFieldU32(sfEmitGeneration) >= 10) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitGeneration was 10 or more."; + return Unexpected(EMISSION_FAILURE); + } + + auto const gen = emitDetails.getFieldU32(sfEmitGeneration); + auto const bur = emitDetails.getFieldU64(sfEmitBurden); + auto const pTxnID = emitDetails.getFieldH256(sfEmitParentTxnID); + auto const nonce = emitDetails.getFieldH256(sfEmitNonce); + + std::optional callback; + if (emitDetails.isFieldPresent(sfEmitCallback)) + callback = emitDetails.getAccountID(sfEmitCallback); + + auto const& hash = emitDetails.getFieldH256(sfEmitHookHash); + + uint32_t gen_proper = static_cast(etxn_generation()); + + if (gen != gen_proper) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitGeneration provided in EmitDetails " + << "not correct (" << gen << ") " + << "should be " << gen_proper; + return Unexpected(EMISSION_FAILURE); + } + + uint64_t bur_proper = static_cast(etxn_burden().value()); + if (bur != bur_proper) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitBurden provided in EmitDetails " + << "was not correct (" << bur << ") " + << "should be " << bur_proper; + return Unexpected(EMISSION_FAILURE); + } + + if (pTxnID != applyCtx.tx.getTransactionID()) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitParentTxnID provided in EmitDetails" + << "was not correct"; + return Unexpected(EMISSION_FAILURE); + } + + if (hookCtx.nonce_used.find(nonce) == hookCtx.nonce_used.end()) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitNonce provided in EmitDetails was not " + "generated by nonce api"; + return Unexpected(EMISSION_FAILURE); + } + + if (callback && *callback != hookCtx.result.account) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfEmitCallback account must be the account of " + "the emitting hook"; + return Unexpected(EMISSION_FAILURE); + } + + if (hash != hookCtx.result.hookHash) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() + << "]: sfEmitHookHash must be the hash of the emitting hook"; + return Unexpected(EMISSION_FAILURE); + } + + // rule 4: sfTxnSignature must be absent + if (stpTrans->isFieldPresent(sfTxnSignature)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfTxnSignature is present but should not be"; + return Unexpected(EMISSION_FAILURE); + } + + // rule 5: LastLedgerSeq must be present and after current ledger + if (!stpTrans->isFieldPresent(sfLastLedgerSequence)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfLastLedgerSequence missing"; + return Unexpected(EMISSION_FAILURE); + } + + uint32_t tx_lls = stpTrans->getFieldU32(sfLastLedgerSequence); + uint32_t ledgerSeq = view.info().seq; + if (tx_lls < ledgerSeq + 1) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() + << "]: sfLastLedgerSequence invalid (less than next ledger)"; + return Unexpected(EMISSION_FAILURE); + } + + if (tx_lls > ledgerSeq + 5) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() + << "]: sfLastLedgerSequence cannot be greater than current seq + 5"; + return Unexpected(EMISSION_FAILURE); + } + + // rule 6 + if (!stpTrans->isFieldPresent(sfFirstLedgerSequence) || + stpTrans->getFieldU32(sfFirstLedgerSequence) > tx_lls) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: sfFirstLedgerSequence must be present and <= " + "LastLedgerSequence"; + return Unexpected(EMISSION_FAILURE); + } + + // rule 7 check the emitted txn pays the appropriate fee + int64_t minfee = etxn_fee_base(txBlob).value(); + + if (minfee < 0) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: Fee could not be calculated"; + return Unexpected(EMISSION_FAILURE); + } + + if (!stpTrans->isFieldPresent(sfFee)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: Fee missing from emitted tx"; + return Unexpected(EMISSION_FAILURE); + } + + int64_t fee = stpTrans->getFieldAmount(sfFee).xrp().drops(); + if (fee < minfee) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: Fee less than minimum required"; + return Unexpected(EMISSION_FAILURE); + } + + std::string reason; + auto tpTrans = + std::make_shared(stpTrans, reason, applyCtx.app); + if (tpTrans->getStatus() != NEW) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: tpTrans->getStatus() != NEW"; + return Unexpected(EMISSION_FAILURE); + } + + // preflight the transaction + auto preflightResult = ripple::preflight( + applyCtx.app, + view.rules(), + *stpTrans, + ripple::ApplyFlags::tapPREFLIGHT_EMIT, + j); + + if (!isTesSuccess(preflightResult.ter)) + { + JLOG(j.trace()) << "HookEmit[" << HC_ACC() + << "]: Transaction preflight failure: " + << transHuman(preflightResult.ter); + return Unexpected(EMISSION_FAILURE); + } + + return tpTrans; +} + +Expected +HookAPI::etxn_burden() const +{ + if (hookCtx.expected_etxn_count <= -1) + return Unexpected(PREREQUISITE_NOT_MET); + + uint64_t last_burden = static_cast(otxn_burden()); + uint64_t burden = + last_burden * static_cast(hookCtx.expected_etxn_count); + if (burden < last_burden) + return Unexpected(FEE_TOO_LARGE); + return burden; +} + +Expected +HookAPI::etxn_fee_base(ripple::Slice const& txBlob) const +{ + auto& applyCtx = hookCtx.applyCtx; + auto j = applyCtx.app.journal("View"); + + if (hookCtx.expected_etxn_count <= -1) + return Unexpected(PREREQUISITE_NOT_MET); + + try + { + SerialIter sitTrans(txBlob); + std::unique_ptr stpTrans = + std::make_unique(std::ref(sitTrans)); + + if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128)) + return Transactor::calculateBaseFee( + *(applyCtx.app.openLedger().current()), *stpTrans) + .drops(); + + return invoke_calculateBaseFee( + *(applyCtx.app.openLedger().current()), *stpTrans) + .drops(); + } + catch (std::exception const& e) + { + JLOG(j.trace()) << "HookInfo[" << HC_ACC() + << "]: etxn_fee_base exception: " << e.what(); + return Unexpected(INVALID_TXN); + } +} + +Expected +HookAPI::etxn_details(uint8_t* out_ptr) const +{ + if (hookCtx.expected_etxn_count <= -1) + return Unexpected(PREREQUISITE_NOT_MET); + + uint32_t generation = etxn_generation(); + + auto const burden_result = etxn_burden(); + + if (!burden_result) + return Unexpected(FEE_TOO_LARGE); + + int64_t burden = burden_result.value(); + + uint8_t* out = out_ptr; + + *out++ = 0xEDU; // begin sfEmitDetails /* upto = + // 0 | size = 1 */ + *out++ = 0x20U; // sfEmitGeneration preamble /* upto = + // 1 | size = 6 */ + *out++ = 0x2EU; // preamble cont + *out++ = (generation >> 24U) & 0xFFU; + *out++ = (generation >> 16U) & 0xFFU; + *out++ = (generation >> 8U) & 0xFFU; + *out++ = (generation >> 0U) & 0xFFU; + *out++ = 0x3DU; // sfEmitBurden preamble /* upto + // = 7 | size = 9 */ + *out++ = (burden >> 56U) & 0xFFU; + *out++ = (burden >> 48U) & 0xFFU; + *out++ = (burden >> 40U) & 0xFFU; + *out++ = (burden >> 32U) & 0xFFU; + *out++ = (burden >> 24U) & 0xFFU; + *out++ = (burden >> 16U) & 0xFFU; + *out++ = (burden >> 8U) & 0xFFU; + *out++ = (burden >> 0U) & 0xFFU; + *out++ = 0x5BU; // sfEmitParentTxnID preamble /* upto + // = 16 | size = 33 */ + auto const& txID = hookCtx.applyCtx.tx.getTransactionID(); + memcpy(out, txID.data(), 32); + out += 32; + *out++ = 0x5CU; // sfEmitNonce /* upto + // = 49 | size = 33 */ + + auto hash = etxn_nonce(); + if (!hash.has_value()) + return INTERNAL_ERROR; + + memcpy(out, hash->data(), 32); + + out += 32; + *out++ = 0x5DU; // sfEmitHookHash preamble /* upto + // = 82 | size = 33 */ + for (int i = 0; i < 32; ++i) + *out++ = hookCtx.result.hookHash.data()[i]; + + if (hookCtx.result.hasCallback) + { + *out++ = 0x8AU; // sfEmitCallback preamble /* + // upto = 115 | size = 22 */ + *out++ = 0x14U; // preamble cont + + memcpy(out, hookCtx.result.account.data(), 20); + + out += 20; + } + *out++ = 0xE1U; // end object (sfEmitDetails) /* upto = + // 137 | size = 1 */ + /* upto = 138 | --------- */ + int64_t outlen = out - out_ptr; + + return outlen; +} + +Expected +HookAPI::etxn_reserve(uint64_t count) const +{ + if (hookCtx.expected_etxn_count > -1) + return Unexpected(ALREADY_SET); + + if (count < 1) + return Unexpected(TOO_SMALL); + + if (count > hook_api::max_emit) + return Unexpected(TOO_BIG); + + hookCtx.expected_etxn_count = count; + + return count; +} + +uint32_t +HookAPI::etxn_generation() const +{ + return otxn_generation() + 1; +} + +Expected +HookAPI::etxn_nonce() const +{ + if (hookCtx.emit_nonce_counter > hook_api::max_nonce) + return Unexpected(TOO_MANY_NONCES); + + // in some cases the same hook might execute multiple times + // on one txn, therefore we need to pass this information to the nonce + uint32_t flags = 0; + flags |= hookCtx.result.isStrong ? 0b10U : 0; + flags |= hookCtx.result.isCallback ? 0b01U : 0; + flags |= (hookCtx.result.hookChainPosition << 2U); + + auto hash = ripple::sha512Half( + ripple::HashPrefix::emitTxnNonce, + hookCtx.applyCtx.tx.getTransactionID(), + hookCtx.emit_nonce_counter++, + hookCtx.result.account, + hookCtx.result.hookHash, + flags); + + hookCtx.nonce_used[hash] = true; + + return hash; +} + +/// float APIs + +using namespace hook_float; + +Expected +HookAPI::float_set(int32_t exponent, int64_t mantissa) const +{ + if (mantissa == 0) + return 0; + + auto normalized = hook_float::normalize_xfl(mantissa, exponent); + + // the above function will underflow into a canonical 0 + // but this api must report that underflow + if (!normalized) + { + if (normalized.error() == XFL_OVERFLOW) + return Unexpected(INVALID_FLOAT); + return normalized.error(); + } + if (normalized.value() == 0) + return Unexpected(INVALID_FLOAT); + + return normalized; +} + +Expected +HookAPI::float_multiply(uint64_t float1, uint64_t float2) const +{ + if (float1 == 0 || float2 == 0) + return 0; + + uint64_t man1 = get_mantissa(float1).value(); + int32_t exp1 = get_exponent(float1).value(); + bool neg1 = is_negative(float1); + uint64_t man2 = get_mantissa(float2).value(); + int32_t exp2 = get_exponent(float2).value(); + bool neg2 = is_negative(float2); + + return float_multiply_internal_parts(man1, exp1, neg1, man2, exp2, neg2); +} + +Expected +HookAPI::float_mulratio( + uint64_t float1, + uint32_t round_up, + uint32_t numerator, + uint32_t denominator) const +{ + if (float1 == 0) + return 0; + if (denominator == 0) + return Unexpected(DIVISION_BY_ZERO); + + int64_t man1 = get_mantissa(float1).value(); + int32_t exp1 = get_exponent(float1).value(); + + if (!mulratio_internal(man1, exp1, round_up > 0, numerator, denominator)) + return Unexpected(XFL_OVERFLOW); + + // defensive check + if (man1 < 0) + man1 *= -1LL; + + auto const result = make_float((uint64_t)man1, exp1, is_negative(float1)); + if (!result) + return result.error(); + return result; +} + +uint64_t +HookAPI::float_negate(uint64_t float1) const +{ + if (float1 == 0) + return 0; + return invert_sign(float1); +} + +Expected +HookAPI::float_compare(uint64_t float1, uint64_t float2, uint32_t mode) const +{ + bool equal_flag = mode & compare_mode::EQUAL; + bool less_flag = mode & compare_mode::LESS; + bool greater_flag = mode & compare_mode::GREATER; + bool not_equal = less_flag && greater_flag; + + if ((equal_flag && less_flag && greater_flag) || mode == 0) + return Unexpected(INVALID_ARGUMENT); + + if (mode & (~0b111UL)) + return Unexpected(INVALID_ARGUMENT); + + try + { + int64_t man1 = + (get_mantissa(float1)).value() * (is_negative(float1) ? -1LL : 1LL); + int32_t exp1 = get_exponent(float1).value(); + ripple::IOUAmount amt1{man1, exp1}; + int64_t man2 = + get_mantissa(float2).value() * (is_negative(float2) ? -1LL : 1LL); + int32_t exp2 = get_exponent(float2).value(); + ripple::IOUAmount amt2{man2, exp2}; + + if (not_equal && amt1 != amt2) + return 1; + + if (equal_flag && amt1 == amt2) + return 1; + + if (greater_flag && amt1 > amt2) + return 1; + + if (less_flag && amt1 < amt2) + return 1; + + return 0; + } + catch (std::overflow_error& e) + { + return Unexpected(XFL_OVERFLOW); + } +} + +Expected +HookAPI::float_sum(uint64_t float1, uint64_t float2) const +{ + if (float1 == 0) + return float2; + if (float2 == 0) + return float1; + + int64_t man1 = + get_mantissa(float1).value() * (is_negative(float1) ? -1LL : 1LL); + int32_t exp1 = get_exponent(float1).value(); + int64_t man2 = + get_mantissa(float2).value() * (is_negative(float2) ? -1LL : 1LL); + int32_t exp2 = get_exponent(float2).value(); + + try + { + ripple::IOUAmount amt1{man1, exp1}; + ripple::IOUAmount amt2{man2, exp2}; + + amt1 += amt2; + auto const result = make_float(amt1); + if (!result) + { + // TODO: Should be (EXPONENT_UNDERSIZED || MANTISSA_UNDERSIZED) + if (result.error() == EXPONENT_UNDERSIZED) + { + // this is an underflow e.g. as a result of subtracting an xfl + // from itself and thus not an error, just return canonical 0 + return 0; + } + return Unexpected(result.error()); + } + return result; + } + catch (std::overflow_error& e) + { + return Unexpected(XFL_OVERFLOW); + } +} + +Expected +HookAPI::float_sto( + std::optional currency, + std::optional issuer, + uint64_t float1, + uint32_t field_code, + uint32_t write_len) const +{ + uint16_t field = field_code & 0xFFFFU; + uint16_t type = field_code >> 16U; + + bool is_xrp = field_code == 0; + bool is_short = + field_code == 0xFFFFFFFFU; // non-xrp value but do not output header or + // tail, just amount + + int bytes_needed = 8 + + (field == 0 && type == 0 + ? 0 + : (field == 0xFFFFU && type == 0xFFFFU + ? 0 + : (field < 16 && type < 16 + ? 1 + : (field >= 16 && type < 16 + ? 2 + : (field < 16 && type >= 16 ? 2 : 3))))); + + if (issuer && !currency) + return Unexpected(INVALID_ARGUMENT); + + if (!issuer && currency) + return Unexpected(INVALID_ARGUMENT); + + if (issuer) + { + if (is_xrp) + return Unexpected(INVALID_ARGUMENT); + if (is_short) + return Unexpected(INVALID_ARGUMENT); + + bytes_needed += 40; + } + else if (!is_xrp && !is_short) + return Unexpected(INVALID_ARGUMENT); + + if (bytes_needed > write_len) + return Unexpected(TOO_SMALL); + + Bytes vec(bytes_needed); + uint8_t* write_ptr = vec.data(); + + if (is_xrp || is_short) + { + // do nothing + } + else if (field < 16 && type < 16) + { + *write_ptr++ = (((uint8_t)type) << 4U) + ((uint8_t)field); + } + else if (field >= 16 && type < 16) + { + *write_ptr++ = (((uint8_t)type) << 4U); + *write_ptr++ = ((uint8_t)field); + } + else if (field < 16 && type >= 16) + { + *write_ptr++ = (((uint8_t)field) << 4U); + *write_ptr++ = ((uint8_t)type); + } + else + { + *write_ptr++ = 0; + *write_ptr++ = ((uint8_t)type); + *write_ptr++ = ((uint8_t)field); + } + + uint64_t man = get_mantissa(float1).value(); + int32_t exp = get_exponent(float1).value(); + bool neg = is_negative(float1); + uint8_t out[8]; + if (is_xrp) + { + int32_t shift = -(exp); + + if (shift > 15) + // https://github.com/Xahau/xahaud/issues/586 + return Unexpected(XFL_OVERFLOW); + + if (shift < 0) + return Unexpected(XFL_OVERFLOW); + + if (shift > 0) + man /= power_of_ten[shift]; + + out[0] = (neg ? 0b00000000U : 0b01000000U); + out[0] += (uint8_t)((man >> 56U) & 0b111111U); + out[1] = (uint8_t)((man >> 48U) & 0xFF); + out[2] = (uint8_t)((man >> 40U) & 0xFF); + out[3] = (uint8_t)((man >> 32U) & 0xFF); + out[4] = (uint8_t)((man >> 24U) & 0xFF); + out[5] = (uint8_t)((man >> 16U) & 0xFF); + out[6] = (uint8_t)((man >> 8U) & 0xFF); + out[7] = (uint8_t)((man >> 0U) & 0xFF); + } + else if (man == 0) + { + out[0] = 0b10000000U; + for (int i = 1; i < 8; ++i) + out[i] = 0; + } + else + { + exp += 97; + + /// encode the rippled floating point sto format + + out[0] = (neg ? 0b10000000U : 0b11000000U); + out[0] += (uint8_t)(exp >> 2U); + out[1] = ((uint8_t)(exp & 0b11U)) << 6U; + out[1] += (((uint8_t)(man >> 48U)) & 0b111111U); + out[2] = (uint8_t)((man >> 40U) & 0xFFU); + out[3] = (uint8_t)((man >> 32U) & 0xFFU); + out[4] = (uint8_t)((man >> 24U) & 0xFFU); + out[5] = (uint8_t)((man >> 16U) & 0xFFU); + out[6] = (uint8_t)((man >> 8U) & 0xFFU); + out[7] = (uint8_t)((man >> 0U) & 0xFFU); + } + + std::memcpy(write_ptr, out, 8); + write_ptr += 8; + + if (!is_xrp && !is_short) + { + std::memcpy(write_ptr, currency->data(), 20); + write_ptr += 20; + std::memcpy(write_ptr, issuer->data(), 20); + } + + return vec; +} + +Expected +HookAPI::float_sto_set(Bytes const& data) const +{ + uint8_t* upto = const_cast(data.data()); + uint8_t length = data.size(); + + if (length > 8) + { + uint8_t hi = upto[0] >> 4U; + uint8_t lo = upto[0] & 0xFU; + + if (hi == 0 && lo == 0) + { + // typecode >= 16 && fieldcode >= 16 + if (length < 11) + return Unexpected(NOT_AN_OBJECT); + upto += 3; + length -= 3; + } + else if (hi == 0 || lo == 0) + { + // typecode >= 16 && fieldcode < 16 + if (length < 10) + return Unexpected(NOT_AN_OBJECT); + upto += 2; + length -= 2; + } + else + { + // typecode < 16 && fieldcode < 16 + upto++; + length--; + } + } + + if (length < 8) + return Unexpected(NOT_AN_OBJECT); + + bool is_xrp = (((*upto) & 0b10000000U) == 0); + bool is_negative = (((*upto) & 0b01000000U) == 0); + + int32_t exponent = 0; + + if (is_xrp) + { + // exponent remains 0 + upto++; + } + else + { + exponent = (((*upto++) & 0b00111111U)) << 2U; + exponent += ((*upto) >> 6U); + exponent -= 97; + } + + uint64_t mantissa = (((uint64_t)(*upto++)) & 0b00111111U) << 48U; + mantissa += ((uint64_t)*upto++) << 40U; + mantissa += ((uint64_t)*upto++) << 32U; + mantissa += ((uint64_t)*upto++) << 24U; + mantissa += ((uint64_t)*upto++) << 16U; + mantissa += ((uint64_t)*upto++) << 8U; + mantissa += ((uint64_t)*upto++); + + if (mantissa == 0) + return 0; + + return hook_float::normalize_xfl(mantissa, exponent, is_negative); +} + +Expected +HookAPI::float_invert(uint64_t float1) const +{ + if (float1 == 0) + return Unexpected(DIVISION_BY_ZERO); + if (float1 == float_one_internal) + return float_one_internal; + + return float_divide_internal(float_one_internal, float1); +} + +Expected +HookAPI::float_divide(uint64_t float1, uint64_t float2) const +{ + return float_divide_internal(float1, float2); +} + +uint64_t +HookAPI::float_one() const +{ + return float_one_internal; +} + +Expected +HookAPI::float_mantissa(uint64_t float1) const +{ + if (float1 == 0) + return 0; + return get_mantissa(float1); +} + +uint64_t +HookAPI::float_sign(uint64_t float1) const +{ + if (float1 == 0) + return 0; + return is_negative(float1); +} + +Expected +HookAPI::float_int(uint64_t float1, uint32_t decimal_places, uint32_t absolute) + const +{ + if (float1 == 0) + return 0; + uint64_t man1 = get_mantissa(float1).value(); + int32_t exp1 = get_exponent(float1).value(); + bool neg1 = is_negative(float1); + + if (decimal_places > 15) + return Unexpected(INVALID_ARGUMENT); + + if (neg1) + { + if (!absolute) + return Unexpected(CANT_RETURN_NEGATIVE); + } + + int32_t shift = -(exp1 + decimal_places); + + if (shift > 15) + return 0; + + if (shift < 0) + return Unexpected(TOO_BIG); + + if (shift > 0) + man1 /= power_of_ten[shift]; + + return man1; +} + +Expected +HookAPI::float_log(uint64_t float1) const +{ + if (float1 == 0) + return Unexpected(INVALID_ARGUMENT); + + uint64_t man1 = get_mantissa(float1).value(); + int32_t exp1 = get_exponent(float1).value(); + if (is_negative(float1)) + return Unexpected(COMPLEX_NOT_SUPPORTED); + + double inp = (double)(man1); + double result = log10(inp) + exp1; + + return double_to_xfl(result); +} + +Expected +HookAPI::float_root(uint64_t float1, uint32_t n) const +{ + if (float1 == 0) + return 0; + + if (n < 2) + return Unexpected(INVALID_ARGUMENT); + + uint64_t man1 = get_mantissa(float1).value(); + int32_t exp1 = get_exponent(float1).value(); + if (is_negative(float1)) + return Unexpected(COMPLEX_NOT_SUPPORTED); + + double inp = (double)(man1)*pow(10, exp1); + double result = pow(inp, ((double)1.0f) / ((double)(n))); + + return double_to_xfl(result); +} + +/// otxn APIs + +uint64_t +HookAPI::otxn_burden() const +{ + auto& applyCtx = hookCtx.applyCtx; + auto j = applyCtx.app.journal("View"); + + if (hookCtx.burden) + return hookCtx.burden; + + auto const& tx = applyCtx.tx; + if (!tx.isFieldPresent(sfEmitDetails)) + return 1; + + auto const& pd = const_cast(tx) + .getField(sfEmitDetails) + .downcast(); + + if (!pd.isFieldPresent(sfEmitBurden)) + { + JLOG(j.warn()) + << "HookError[" << HC_ACC() + << "]: found sfEmitDetails but sfEmitBurden was not present"; + return 1; + } + + uint64_t burden = pd.getFieldU64(sfEmitBurden); + burden &= ((1ULL << 63) - 1); + hookCtx.burden = burden; + return static_cast(burden); +} + +uint32_t +HookAPI::otxn_generation() const +{ + auto& applyCtx = hookCtx.applyCtx; + auto j = applyCtx.app.journal("View"); + + if (hookCtx.generation) + return hookCtx.generation; + + auto const& tx = applyCtx.tx; + if (!tx.isFieldPresent(sfEmitDetails)) + return 0; + + auto const& pd = const_cast(tx) + .getField(sfEmitDetails) + .downcast(); + + if (!pd.isFieldPresent(sfEmitGeneration)) + { + JLOG(j.warn()) + << "HookError[" << HC_ACC() + << "]: found sfEmitDetails but sfEmitGeneration was not present"; + return 0; + } + + hookCtx.generation = pd.getFieldU32(sfEmitGeneration); + return hookCtx.generation; +} + +Expected +HookAPI::otxn_field(uint32_t field_id) const +{ + SField const& fieldType = ripple::SField::getField(field_id); + + if (fieldType == sfInvalid) + return Unexpected(INVALID_FIELD); + + if (!hookCtx.applyCtx.tx.isFieldPresent(fieldType)) + return Unexpected(DOESNT_EXIST); + + auto const& field = hookCtx.emitFailure + ? hookCtx.emitFailure->getField(fieldType) + : const_cast(hookCtx.applyCtx.tx).getField(fieldType); + + return &field; +} + +Expected +HookAPI::otxn_id(uint32_t flags) const +{ + auto const& txID = + (hookCtx.emitFailure && !flags + ? hookCtx.applyCtx.tx.getFieldH256(sfTransactionHash) + : hookCtx.applyCtx.tx.getTransactionID()); + + return txID; +} + +TxType +HookAPI::otxn_type() const +{ + if (hookCtx.emitFailure) + return safe_cast( + hookCtx.emitFailure->getFieldU16(sfTransactionType)); + + return hookCtx.applyCtx.tx.getTxnType(); +} + +Expected +HookAPI::otxn_slot(uint32_t slot_into) const +{ + if (slot_into > hook_api::max_slots) + return Unexpected(INVALID_ARGUMENT); + + // check if we can emplace the object to a slot + if (slot_into == 0 && no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + if (slot_into == 0) + { + if (auto found = get_free_slot(); found) + slot_into = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + auto const& st_tx = std::make_shared( + hookCtx.emitFailure ? *(hookCtx.emitFailure) + : const_cast(hookCtx.applyCtx.tx) + .downcast()); + + hookCtx.slot[slot_into] = hook::SlotEntry{.storage = st_tx, .entry = 0}; + + hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); + + return slot_into; +} + +Expected +HookAPI::otxn_param(Bytes const& param_name) const +{ + if (param_name.size() < 1) + return Unexpected(TOO_SMALL); + + if (param_name.size() > 32) + return Unexpected(TOO_BIG); + + if (!hookCtx.applyCtx.tx.isFieldPresent(sfHookParameters)) + return Unexpected(DOESNT_EXIST); + + auto const& params = hookCtx.applyCtx.tx.getFieldArray(sfHookParameters); + + for (auto const& param : params) + { + if (!param.isFieldPresent(sfHookParameterName) || + param.getFieldVL(sfHookParameterName) != param_name) + continue; + + if (!param.isFieldPresent(sfHookParameterValue)) + return Unexpected(DOESNT_EXIST); + + auto const& val = param.getFieldVL(sfHookParameterValue); + if (val.empty()) + return Unexpected(DOESNT_EXIST); + + return val; + } + + return Unexpected(DOESNT_EXIST); +} + +/// hook APIs + +AccountID +HookAPI::hook_account() const +{ + return hookCtx.result.account; +} + +Expected +HookAPI::hook_hash(int32_t hook_no) const +{ + if (hook_no == -1) + return hookCtx.result.hookHash; + + std::shared_ptr hookSLE = + hookCtx.applyCtx.view().peek(hookCtx.result.hookKeylet); + if (!hookSLE || !hookSLE->isFieldPresent(sfHooks)) + return Unexpected(INTERNAL_ERROR); + + ripple::STArray const& hooks = hookSLE->getFieldArray(sfHooks); + if (hook_no >= hooks.size()) + return Unexpected(DOESNT_EXIST); + + auto const& hook = hooks[hook_no]; + if (!hook.isFieldPresent(sfHookHash)) + return Unexpected(DOESNT_EXIST); + + return hook.getFieldH256(sfHookHash); +} + +Expected +HookAPI::hook_again() const +{ + if (hookCtx.result.executeAgainAsWeak) + return Unexpected(ALREADY_SET); + + if (hookCtx.result.isStrong) + { + hookCtx.result.executeAgainAsWeak = true; + return 1; + } + + return Unexpected(PREREQUISITE_NOT_MET); +} + +Expected +HookAPI::hook_param(Bytes const& paramName) const +{ + if (paramName.size() < 1) + return Unexpected(TOO_SMALL); + + if (paramName.size() > 32) + return Unexpected(TOO_BIG); + + // first check for overrides set by prior hooks in the chain + auto const& overrides = hookCtx.result.hookParamOverrides; + if (overrides.find(hookCtx.result.hookHash) != overrides.end()) + { + auto const& params = overrides.at(hookCtx.result.hookHash); + if (params.find(paramName) != params.end()) + { + auto const& param = params.at(paramName); + if (param.size() == 0) + // allow overrides to "delete" parameters + return Unexpected(DOESNT_EXIST); + + return param; + } + } + + // next check if there's a param set on this hook + auto const& params = hookCtx.result.hookParams; + if (params.find(paramName) != params.end()) + { + auto const& param = params.at(paramName); + if (param.size() == 0) + return Unexpected(DOESNT_EXIST); + + return param; + } + + return Unexpected(DOESNT_EXIST); +} + +Expected +HookAPI::hook_param_set( + uint256 const& hash, + Bytes const& paramName, + Bytes const& paramValue) const +{ + if (paramName.size() < 1) + return Unexpected(TOO_SMALL); + + if (paramName.size() > hook::maxHookParameterKeySize()) + return Unexpected(TOO_BIG); + + if (paramValue.size() > hook::maxHookParameterValueSize()) + return Unexpected(TOO_BIG); + + if (hookCtx.result.overrideCount >= hook_api::max_params) + return Unexpected(TOO_MANY_PARAMS); + + hookCtx.result.overrideCount++; + + auto& overrides = hookCtx.result.hookParamOverrides; + if (overrides.find(hash) == overrides.end()) + { + overrides[hash] = std::map{ + {std::move(paramName), std::move(paramValue)}}; + } + else + overrides[hash][std::move(paramName)] = std::move(paramValue); + + return paramValue.size(); +} + +Expected +HookAPI::hook_skip(uint256 const& hash, uint32_t flags) const +{ + if (flags != 0 && flags != 1) + return Unexpected(INVALID_ARGUMENT); + + auto& skips = hookCtx.result.hookSkips; + + if (flags == 1) + { + // delete flag + if (skips.find(hash) == skips.end()) + return Unexpected(DOESNT_EXIST); + skips.erase(hash); + return 1; + } + + // first check if it's already in the skips set + if (skips.find(hash) != skips.end()) + return 1; + + // next check if it's even in this chain + std::shared_ptr hookSLE = + hookCtx.applyCtx.view().peek(hookCtx.result.hookKeylet); + + if (!hookSLE || !hookSLE->isFieldPresent(sfHooks)) + return Unexpected(INTERNAL_ERROR); + + ripple::STArray const& hooks = hookSLE->getFieldArray(sfHooks); + bool found = false; + for (auto const& hookObj : hooks) + { + if (hookObj.isFieldPresent(sfHookHash)) + { + if (hookObj.getFieldH256(sfHookHash) == hash) + { + found = true; + break; + } + } + } + + if (!found) + return Unexpected(DOESNT_EXIST); + + // finally add it to the skips list + hookCtx.result.hookSkips.emplace(hash); + return 1; +} + +uint8_t +HookAPI::hook_pos() const +{ + return hookCtx.result.hookChainPosition; +} + +/// ledger APIs + +uint64_t +HookAPI::fee_base() const +{ + return hookCtx.applyCtx.view().fees().base.drops(); +} + +uint32_t +HookAPI::ledger_seq() const +{ + return hookCtx.applyCtx.view().info().seq; +} + +uint256 +HookAPI::ledger_last_hash() const +{ + return hookCtx.applyCtx.view().info().parentHash; +} + +uint64_t +HookAPI::ledger_last_time() const +{ + return hookCtx.applyCtx.view() + .info() + .parentCloseTime.time_since_epoch() + .count(); +} + +Expected +HookAPI::ledger_nonce() const +{ + auto& view = hookCtx.applyCtx.view(); + if (hookCtx.ledger_nonce_counter > hook_api::max_nonce) + return Unexpected(TOO_MANY_NONCES); + + auto hash = ripple::sha512Half( + ripple::HashPrefix::hookNonce, + view.info().seq, + view.info().parentCloseTime.time_since_epoch().count(), + view.info().parentHash, + hookCtx.applyCtx.tx.getTransactionID(), + hookCtx.ledger_nonce_counter++, + hookCtx.result.account); + + return hash; +} + +Expected +HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const +{ + // keylets must be the same type! + if (klLo.type != klHi.type) + return Unexpected(DOES_NOT_MATCH); + + std::optional found = + hookCtx.applyCtx.view().succ(klLo.key, klHi.key.next()); + + if (!found) + return Unexpected(DOESNT_EXIST); + + Keylet kl_out{klLo.type, *found}; + + return kl_out; +} + +/// state APIs + +// state + +Expected +HookAPI::state_foreign( + uint256 const& key, + uint256 const& ns, + AccountID const& account) const +{ + // first check if the requested state was previously cached this session + auto cacheEntryLookup = lookup_state_cache(account, ns, key); + if (cacheEntryLookup) + { + auto const& cacheEntry = cacheEntryLookup->get(); + + return cacheEntry.second; + } + + auto hsSLE = + hookCtx.applyCtx.view().peek(keylet::hookState(account, key, ns)); + + if (!hsSLE) + return Unexpected(DOESNT_EXIST); + + Blob b = hsSLE->getFieldVL(sfHookStateData); + + // it exists add it to cache and return it + if (!set_state_cache(account, ns, key, b, false).has_value()) + return Unexpected(INTERNAL_ERROR); // should never happen + + return b; +} + +// state_set + +Expected +HookAPI::state_foreign_set( + uint256 const& key, + uint256 const& ns, + AccountID const& account, + Bytes& data) const +{ + // local modifications are always allowed + if (account == hookCtx.result.account) + { + if (auto ret = set_state_cache(account, ns, key, data, true); + !ret.has_value()) + return Unexpected(ret.error()); + + return data.size(); + } + + // execution to here means it's actually a foreign set + if (hookCtx.result.foreignStateSetDisabled) + return Unexpected(PREVIOUS_FAILURE_PREVENTS_RETRY); + + // first check if we've already modified this state + auto cacheEntry = lookup_state_cache(account, ns, key); + if (cacheEntry && cacheEntry->get().first) + { + // if a cache entry already exists and it has already been modified + // don't check grants again + if (auto ret = set_state_cache(account, ns, key, data, true); + !ret.has_value()) + return Unexpected(ret.error()); + + return data.size(); + } + + // cache miss or cache was present but entry was not marked as previously + // modified therefore before continuing we need to check grants + auto const sle = + hookCtx.applyCtx.view().read(ripple::keylet::hook(account)); + if (!sle) + return Unexpected(INTERNAL_ERROR); + + bool found_auth = false; + + // we do this by iterating the hooks installed on the foreign account and in + // turn their grants and namespaces + auto const& hooks = sle->getFieldArray(sfHooks); + for (auto const& hookObj : hooks) + { + // skip blank entries + if (!hookObj.isFieldPresent(sfHookHash)) + continue; + + if (!hookObj.isFieldPresent(sfHookGrants)) + continue; + + auto const& hookGrants = hookObj.getFieldArray(sfHookGrants); + + if (hookGrants.size() < 1) + continue; + + // the grant allows the hook to modify the granter's namespace only + if (hookObj.isFieldPresent(sfHookNamespace)) + { + if (hookObj.getFieldH256(sfHookNamespace) != ns) + continue; + } + else + { + // fetch the hook definition + auto const def = + hookCtx.applyCtx.view().read(ripple::keylet::hookDefinition( + hookObj.getFieldH256(sfHookHash))); + if (!def) // should never happen except in a rare race condition + continue; + if (def->getFieldH256(sfHookNamespace) != ns) + continue; + } + + // this is expensive search so we'll disallow after one failed attempt + for (auto const& hookGrantObj : hookGrants) + { + bool hasAuthorizedField = hookGrantObj.isFieldPresent(sfAuthorize); + + if (hookGrantObj.getFieldH256(sfHookHash) == + hookCtx.result.hookHash && + (!hasAuthorizedField || + hookGrantObj.getAccountID(sfAuthorize) == + hookCtx.result.account)) + { + found_auth = true; + break; + } + } + + if (found_auth) + break; + } + + if (!found_auth) + { + // hook only gets one attempt + hookCtx.result.foreignStateSetDisabled = true; + return Unexpected(NOT_AUTHORIZED); + } + + if (auto ret = set_state_cache(account, ns, key, data, true); + !ret.has_value()) + return Unexpected(ret.error()); + + return data.size(); +} + +/// slot APIs + +Expected +HookAPI::slot(uint32_t slot_no) const +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (hookCtx.slot[slot_no].entry == 0) + return Unexpected(INTERNAL_ERROR); + + return hookCtx.slot[slot_no].entry; +} + +Expected +HookAPI::slot_clear(uint32_t slot_no) const +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + hookCtx.slot.erase(slot_no); + hookCtx.slot_free.push(slot_no); + return 1; +} + +Expected +HookAPI::slot_count(uint32_t slot_no) const +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (hookCtx.slot[slot_no].entry == 0) + return Unexpected(INTERNAL_ERROR); + + if (hookCtx.slot[slot_no].entry->getSType() != STI_ARRAY) + return Unexpected(NOT_AN_ARRAY); + + return hookCtx.slot[slot_no].entry->downcast().size(); +} + +Expected +HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const +{ + if ((data.size() != 32 && data.size() != 34) || + slot_no > hook_api::max_slots) + return Unexpected(INVALID_ARGUMENT); + + if (slot_no == 0 && no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + std::optional> slot_value = + std::nullopt; + + if (data.size() == 34) + { + std::optional kl = unserialize_keylet(data); + if (!kl) + return Unexpected(DOESNT_EXIST); + + if (kl->key == beast::zero) + return Unexpected(DOESNT_EXIST); + + auto const sle = hookCtx.applyCtx.view().read(*kl); + if (!sle) + return Unexpected(DOESNT_EXIST); + + slot_value = sle; + } + else if (data.size() == 32) + { + uint256 hash = uint256::fromVoid(data.data()); + + ripple::error_code_i ec{ripple::error_code_i::rpcUNKNOWN}; + + auto hTx = hookCtx.applyCtx.app.getMasterTransaction().fetch(hash, ec); + + if (auto const* p = std::get_if, + std::shared_ptr>>(&hTx)) + slot_value = p->first->getSTransaction(); + else + return Unexpected(DOESNT_EXIST); + } + else + return Unexpected(INVALID_ARGUMENT); + + if (!slot_value.has_value()) + return Unexpected(DOESNT_EXIST); + + if (slot_no == 0) + { + if (auto found = get_free_slot(); found) + slot_no = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + hookCtx.slot[slot_no] = hook::SlotEntry{.storage = *slot_value, .entry = 0}; + hookCtx.slot[slot_no].entry = &(*hookCtx.slot[slot_no].storage); + + return slot_no; +} + +Expected +HookAPI::slot_size(uint32_t slot_no) const +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (hookCtx.slot[slot_no].entry == 0) + return Unexpected(INTERNAL_ERROR); + + // RH TODO: this is a very expensive way of computing size, cache it + Serializer s; + hookCtx.slot[slot_no].entry->add(s); + return s.getDataLength(); +} + +Expected +HookAPI::slot_subarray( + uint32_t parent_slot, + uint32_t array_id, + uint32_t new_slot) const +{ + if (hookCtx.slot.find(parent_slot) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (hookCtx.slot[parent_slot].entry == 0) + return Unexpected(INTERNAL_ERROR); + + if (hookCtx.slot[parent_slot].entry->getSType() != STI_ARRAY) + return Unexpected(NOT_AN_ARRAY); + + if (new_slot == 0 && no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + if (new_slot > hook_api::max_slots) + return Unexpected(INVALID_ARGUMENT); + + bool copied = false; + try + { + ripple::STArray& parent_obj = + const_cast(*hookCtx.slot[parent_slot].entry) + .downcast(); + + if (parent_obj.size() <= array_id) + return Unexpected(DOESNT_EXIST); + + if (new_slot == 0) + { + if (auto found = get_free_slot(); found) + new_slot = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + // copy + if (new_slot != parent_slot) + { + copied = true; + hookCtx.slot[new_slot] = hookCtx.slot[parent_slot]; + } + hookCtx.slot[new_slot].entry = &(parent_obj[array_id]); + return new_slot; + } + catch (const std::bad_cast& e) + { + if (copied) + { + hookCtx.slot.erase(new_slot); + hookCtx.slot_free.push(new_slot); + } + return Unexpected(NOT_AN_ARRAY); + } + + return new_slot; +} + +Expected +HookAPI::slot_subfield( + uint32_t parent_slot, + uint32_t field_id, + uint32_t new_slot) const +{ + if (hookCtx.slot.find(parent_slot) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (new_slot == 0 && no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + if (new_slot > hook_api::max_slots) + return Unexpected(INVALID_ARGUMENT); + + SField const& fieldCode = ripple::SField::getField(field_id); + + if (fieldCode == sfInvalid) + return Unexpected(INVALID_FIELD); + + if (hookCtx.slot[parent_slot].entry == 0) + return Unexpected(INTERNAL_ERROR); + + bool copied = false; + + try + { + ripple::STObject& parent_obj = + const_cast(*hookCtx.slot[parent_slot].entry) + .downcast(); + + if (!parent_obj.isFieldPresent(fieldCode)) + return Unexpected(DOESNT_EXIST); + + if (new_slot == 0) + { + if (auto found = get_free_slot(); found) + new_slot = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + // copy + if (new_slot != parent_slot) + { + copied = true; + hookCtx.slot[new_slot] = hookCtx.slot[parent_slot]; + } + + hookCtx.slot[new_slot].entry = &(parent_obj.getField(fieldCode)); + return new_slot; + } + catch (const std::bad_cast& e) + { + if (copied) + { + hookCtx.slot.erase(new_slot); + hookCtx.slot_free.push(new_slot); + } + return Unexpected(NOT_AN_OBJECT); + } +} + +Expected, HookReturnCode> +HookAPI::slot_type(uint32_t slot_no, uint32_t flags) const +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (hookCtx.slot[slot_no].entry == 0) + return Unexpected(INTERNAL_ERROR); + try + { + ripple::STBase& obj = const_cast( + *hookCtx.slot[slot_no].entry); //.downcast(); + if (flags == 0) + return obj; + + // this flag is for use with an amount field to determine if the amount + // is native (xrp) + if (flags == 1) + { + if (obj.getSType() != STI_AMOUNT) + return Unexpected(NOT_AN_AMOUNT); + return const_cast(*hookCtx.slot[slot_no].entry) + .downcast(); + } + + return Unexpected(INVALID_ARGUMENT); + } + catch (const std::bad_cast& e) + { + return Unexpected(INTERNAL_ERROR); + } +} + +Expected +HookAPI::slot_float(uint32_t slot_no) const +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return Unexpected(DOESNT_EXIST); + + if (hookCtx.slot[slot_no].entry == 0) + return Unexpected(INTERNAL_ERROR); + + try + { + ripple::STAmount& st_amt = + const_cast(*hookCtx.slot[slot_no].entry) + .downcast(); + + int64_t normalized = 0; + if (st_amt.native()) + { + ripple::XRPAmount amt = st_amt.xrp(); + int64_t drops = amt.drops(); + int32_t exp = -6; + // normalize + auto const ret = hook_float::normalize_xfl(drops, exp); + if (!ret) + { + if (ret.error() == EXPONENT_UNDERSIZED) + return 0; + return Unexpected(ret.error()); + } + normalized = ret.value(); + } + else + { + ripple::IOUAmount amt = st_amt.iou(); + auto const ret = make_float(amt); + if (!ret) + { + if (ret.error() == EXPONENT_UNDERSIZED) + return 0; + return Unexpected(ret.error()); + } + normalized = ret.value(); + } + + if (normalized == EXPONENT_UNDERSIZED) + /* exponent undersized (underflow) */ + return 0; // return 0 in this case + return normalized; + } + catch (const std::bad_cast& e) + { + return Unexpected(NOT_AN_AMOUNT); + } +} + +/// trace APIs +// trace +// trace_num +// trace_float + +Expected +HookAPI::meta_slot(uint32_t slot_into) const +{ + if (!hookCtx.result.provisionalMeta) + return Unexpected(PREREQUISITE_NOT_MET); + + if (slot_into > hook_api::max_slots) + return Unexpected(INVALID_ARGUMENT); + + // check if we can emplace the object to a slot + if (slot_into == 0 && no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + if (slot_into == 0) + { + if (auto found = get_free_slot(); found) + slot_into = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + hookCtx.slot[slot_into] = + hook::SlotEntry{.storage = hookCtx.result.provisionalMeta, .entry = 0}; + + hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); + + return slot_into; +} + +Expected, HookReturnCode> +HookAPI::xpop_slot(uint32_t slot_into_tx, uint32_t slot_into_meta) const +{ + if (hookCtx.applyCtx.tx.getFieldU16(sfTransactionType) != ttIMPORT) + return Unexpected(PREREQUISITE_NOT_MET); + + if (slot_into_tx > hook_api::max_slots || + slot_into_meta > hook_api::max_slots) + return Unexpected(INVALID_ARGUMENT); + + size_t free_count = hook_api::max_slots - hookCtx.slot.size(); + + size_t needed_count = slot_into_tx == 0 && slot_into_meta == 0 + ? 2 + : slot_into_tx != 0 && slot_into_meta != 0 ? 0 : 1; + + if (free_count < needed_count) + return Unexpected(NO_FREE_SLOTS); + + // if they supply the same slot number for both (other than 0) + // they will produce a collision + if (needed_count == 0 && slot_into_tx == slot_into_meta) + return Unexpected(INVALID_ARGUMENT); + + if (slot_into_tx == 0) + { + if (no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + if (auto found = get_free_slot(); found) + slot_into_tx = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + if (slot_into_meta == 0) + { + if (no_free_slots()) + return Unexpected(NO_FREE_SLOTS); + + if (auto found = get_free_slot(); found) + slot_into_meta = *found; + else + return Unexpected(NO_FREE_SLOTS); + } + + auto [tx, meta] = + Import::getInnerTxn(hookCtx.applyCtx.tx, hookCtx.applyCtx.journal); + + if (!tx || !meta) + return Unexpected(INVALID_TXN); + + hookCtx.slot[slot_into_tx] = + hook::SlotEntry{.storage = std::move(tx), .entry = 0}; + + hookCtx.slot[slot_into_tx].entry = &(*hookCtx.slot[slot_into_tx].storage); + + hookCtx.slot[slot_into_meta] = + hook::SlotEntry{.storage = std::move(meta), .entry = 0}; + + hookCtx.slot[slot_into_meta].entry = + &(*hookCtx.slot[slot_into_meta].storage); + + return std::make_pair(slot_into_tx, slot_into_meta); +} + +/// private + +inline int32_t +HookAPI::no_free_slots() const +{ + return hook_api::max_slots - hookCtx.slot.size() <= 0; +} + +inline std::optional +HookAPI::get_free_slot() const +{ + // allocate a slot + int32_t slot_into = 0; + if (hookCtx.slot_free.size() > 0) + { + slot_into = hookCtx.slot_free.front(); + hookCtx.slot_free.pop(); + return slot_into; + } + + // no slots were available in the queue so increment slot counter until we + // find a free slot usually this will be the next available but the hook + // developer may have allocated any slot ahead of when the counter gets + // there + do + { + slot_into = ++hookCtx.slot_counter; + } while (hookCtx.slot.find(slot_into) != hookCtx.slot.end() && + // this condition should always be met, if for some reason, somehow + // it is not then we will return the final slot every time. + hookCtx.slot_counter <= hook_api::max_slots); + + if (hookCtx.slot_counter > hook_api::max_slots) + return {}; + + return slot_into; +} + +inline Expected +HookAPI::float_multiply_internal_parts( + uint64_t man1, + int32_t exp1, + bool neg1, + uint64_t man2, + int32_t exp2, + bool neg2) const +{ + using namespace boost::multiprecision; + cpp_int mult = cpp_int(man1) * cpp_int(man2); + mult /= power_of_ten[15]; + uint64_t man_out = static_cast(mult); + if (mult > man_out) + return Unexpected(XFL_OVERFLOW); + + int32_t exp_out = exp1 + exp2 + 15; + bool neg_out = (neg1 && !neg2) || (!neg1 && neg2); + auto const ret = normalize_xfl(man_out, exp_out, neg_out); + + if (!ret) + { + if (ret.error() == EXPONENT_UNDERSIZED) + return 0; + if (ret.error() == EXPONENT_OVERSIZED) + return Unexpected(XFL_OVERFLOW); + return Unexpected(ret.error()); + } + return ret; +} + +inline Expected +HookAPI::mulratio_internal( + int64_t& man1, + int32_t& exp1, + bool round_up, + uint32_t numerator, + uint32_t denominator) const +{ + try + { + ripple::IOUAmount amt{man1, exp1}; + ripple::IOUAmount out = ripple::mulRatio( + amt, numerator, denominator, round_up != 0); // already normalized + man1 = out.mantissa(); + exp1 = out.exponent(); + return 1; + } + catch (std::overflow_error& e) + { + return Unexpected(XFL_OVERFLOW); + } +} + +inline Expected +HookAPI::float_divide_internal(uint64_t float1, uint64_t float2) const +{ + bool const hasFix = hookCtx.applyCtx.view().rules().enabled(fixFloatDivide); + if (float2 == 0) + return Unexpected(DIVISION_BY_ZERO); + if (float1 == 0) + return 0; + + // special case: division by 1 + // RH TODO: add more special cases (division by power of 10) + if (float2 == float_one_internal) + return float1; + + uint64_t man1 = get_mantissa(float1).value(); + int32_t exp1 = get_exponent(float1).value(); + bool neg1 = is_negative(float1); + uint64_t man2 = get_mantissa(float2).value(); + int32_t exp2 = get_exponent(float2).value(); + bool neg2 = is_negative(float2); + + auto tmp1 = normalize_xfl(man1, exp1); + auto tmp2 = normalize_xfl(man2, exp2); + + if (!tmp1 || !tmp2) + return Unexpected(INVALID_FLOAT); + + if (tmp1.value() == 0) + return 0; + + while (man2 > man1) + { + man2 /= 10; + exp2++; + } + + if (man2 == 0) + return Unexpected(DIVISION_BY_ZERO); + + while (man2 < man1) + { + if (man2 * 10 > man1) + break; + man2 *= 10; + exp2--; + } + + uint64_t man3 = 0; + int32_t exp3 = exp1 - exp2; + + while (man2 > 0) + { + int i = 0; + if (hasFix) + { + for (; man1 >= man2; man1 -= man2, ++i) + ; + } + else + { + for (; man1 > man2; man1 -= man2, ++i) + ; + } + + man3 *= 10; + man3 += i; + man2 /= 10; + if (man2 == 0) + break; + exp3--; + } + + bool neg3 = !((neg1 && neg2) || (!neg1 && !neg2)); + + return normalize_xfl(man3, exp3, neg3); +} + +inline Expected +HookAPI::double_to_xfl(double x) const +{ + if ((x) == 0) + return 0; + bool neg = x < 0; + double absresult = neg ? -x : x; + + // first compute the base 10 order of the float + int32_t exp_out = (int32_t)log10(absresult); + + // next adjust it into the valid mantissa range (this means dividing by its + // order and multiplying by 10**15) + absresult *= pow(10, -exp_out + 15); + + // after adjustment the value may still fall below the minMantissa + int64_t result = (int64_t)absresult; + if (result < minMantissa) + { + if (result == minMantissa - 1LL) + result += 1LL; + else + { + result *= 10LL; + exp_out--; + } + } + + // likewise the value can fall above the maxMantissa + if (result > maxMantissa) + { + if (result == maxMantissa + 1LL) + result -= 1LL; + else + { + result /= 10LL; + exp_out++; + } + } + + exp_out -= 15; + auto const ret = make_float(result, exp_out, neg); + + if (!ret) + { + // TODO: Should be (EXPONENT_UNDERSIZED || MANTISSA_UNDERSIZED) + if (ret.error() == EXPONENT_UNDERSIZED) + return 0; + return Unexpected(ret.error()); + } + + return ret; +} + +std::optional +HookAPI::unserialize_keylet(Bytes const& data) const +{ + if (data.size() != 34) + return {}; + + uint16_t ktype = ((uint16_t)data[0] << 8) + ((uint16_t)data[1]); + + return ripple::Keylet{ + static_cast(ktype), + ripple::uint256::fromVoid(data.data() + 2)}; +} + +inline std::optional< + std::reference_wrapper const>> +HookAPI::lookup_state_cache( + AccountID const& acc, + uint256 const& ns, + uint256 const& key) const +{ + auto& stateMap = hookCtx.result.stateMap; + if (stateMap.find(acc) == stateMap.end()) + return std::nullopt; + + auto& stateMapAcc = std::get<3>(stateMap[acc]); + if (stateMapAcc.find(ns) == stateMapAcc.end()) + return std::nullopt; + + auto& stateMapNs = stateMapAcc[ns]; + + auto const& ret = stateMapNs.find(key); + + if (ret == stateMapNs.end()) + return std::nullopt; + + return std::cref(ret->second); +} + +// update the state cache +inline Expected +HookAPI::set_state_cache( + AccountID const& acc, + uint256 const& ns, + uint256 const& key, + Bytes const& data, + bool modified) const +{ + auto& stateMap = hookCtx.result.stateMap; + auto& view = hookCtx.applyCtx.view(); + + if (modified && stateMap.modified_entry_count >= max_state_modifications) + return Unexpected(TOO_MANY_STATE_MODIFICATIONS); + + bool const createNamespace = view.rules().enabled(fixXahauV1) && + !view.exists(keylet::hookStateDir(acc, ns)); + + if (stateMap.find(acc) == stateMap.end()) + { + // if this is the first time this account has been interacted with + // we will compute how many available reserve positions there are + auto const& fees = hookCtx.applyCtx.view().fees(); + + auto const accSLE = view.read(ripple::keylet::account(acc)); + + if (!accSLE) + return Unexpected(DOESNT_EXIST); + + STAmount bal = accSLE->getFieldAmount(sfBalance); + + uint16_t const hookStateScale = accSLE->isFieldPresent(sfHookStateScale) + ? accSLE->getFieldU16(sfHookStateScale) + : 1; + + int64_t availableForReserves = bal.xrp().drops() - + fees.accountReserve(accSLE->getFieldU32(sfOwnerCount)).drops(); + + int64_t increment = fees.increment.drops(); + + if (increment <= 0) + increment = 1; + + availableForReserves /= increment; + + if (availableForReserves < hookStateScale && modified) + return Unexpected(RESERVE_INSUFFICIENT); + + int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces) + ? accSLE->getFieldV256(sfHookNamespaces).size() + : 0; + + if (createNamespace) + { + // overflow should never ever happen but check anyway + if (namespaceCount + 1 < namespaceCount) + return Unexpected(INTERNAL_ERROR); + + if (++namespaceCount > hook::maxNamespaces()) + return Unexpected(TOO_MANY_NAMESPACES); + } + + stateMap.modified_entry_count++; + + // sanity check + if (view.rules().enabled(featureExtendedHookState) && + availableForReserves < hookStateScale) + return Unexpected(INTERNAL_ERROR); + + stateMap[acc] = { + availableForReserves - hookStateScale, + namespaceCount, + hookStateScale, + {{ns, {{key, {modified, data}}}}}}; + return 1; + } + + auto& availableForReserves = std::get<0>(stateMap[acc]); + auto& namespaceCount = std::get<1>(stateMap[acc]); + auto& hookStateScale = std::get<2>(stateMap[acc]); + auto& stateMapAcc = std::get<3>(stateMap[acc]); + bool const canReserveNew = availableForReserves >= hookStateScale; + + if (stateMapAcc.find(ns) == stateMapAcc.end()) + { + if (modified) + { + if (!canReserveNew) + return Unexpected(RESERVE_INSUFFICIENT); + + if (createNamespace) + { + // overflow should never ever happen but check anyway + if (namespaceCount + 1 < namespaceCount) + return Unexpected(INTERNAL_ERROR); + + if (namespaceCount + 1 > hook::maxNamespaces()) + return Unexpected(TOO_MANY_NAMESPACES); + + namespaceCount++; + } + + if (view.rules().enabled(featureExtendedHookState) && + availableForReserves < hookStateScale) + return Unexpected(INTERNAL_ERROR); + + availableForReserves -= hookStateScale; + stateMap.modified_entry_count++; + } + + stateMapAcc[ns] = {{key, {modified, data}}}; + + return 1; + } + + auto& stateMapNs = stateMapAcc[ns]; + if (stateMapNs.find(key) == stateMapNs.end()) + { + if (modified) + { + if (!canReserveNew) + return Unexpected(RESERVE_INSUFFICIENT); + + if (view.rules().enabled(featureExtendedHookState) && + availableForReserves < hookStateScale) + return Unexpected(INTERNAL_ERROR); + + availableForReserves -= hookStateScale; + stateMap.modified_entry_count++; + } + + stateMapNs[key] = {modified, data}; + hookCtx.result.changedStateCount++; + return 1; + } + + if (modified) + { + if (!stateMapNs[key].first) + hookCtx.result.changedStateCount++; + + stateMap.modified_entry_count++; + stateMapNs[key].first = true; + } + + stateMapNs[key].second = data; + return 1; +} + +// RH NOTE this is a light-weight stobject parsing function for drilling into a +// provided serialzied object however it could probably be replaced by an +// existing class or routine or set of routines in XRPLD Returns object length +// including header bytes (and footer bytes in the event of array or object) +// negative indicates error +inline Expected< + int32_t, + HookAPI::parse_error> +HookAPI::get_stobject_length( + unsigned char* start, // in - begin iterator + unsigned char* maxptr, // in - end iterator + int& type, // out - populated by serialized type code + int& field, // out - populated by serialized field code + int& payload_start, // out - the start of actual payload data for this type + int& payload_length, // out - the length of actual payload data for this + // type + int recursion_depth) // used internally + const +{ + if (recursion_depth > 10) + return Unexpected(pe_excessive_nesting); + + unsigned char* end = maxptr; + unsigned char* upto = start; + int high = *upto >> 4; + int low = *upto & 0xF; + + upto++; + if (upto >= end) + return Unexpected(pe_unexpected_end); + if (high > 0 && low > 0) + { + // common type common field + type = high; + field = low; + } + else if (high > 0) + { + // common type, uncommon field + type = high; + field = *upto++; + } + else if (low > 0) + { + // common field, uncommon type + field = low; + type = *upto++; + } + else + { + // uncommon type and field + type = *upto++; + if (upto >= end) + return Unexpected(pe_unexpected_end); + field = *upto++; + } + + DBG_PRINTF( + "%d get_st_object found field %d type %d\n", + recursion_depth, + field, + type); + + if (upto >= end) + return Unexpected(pe_unexpected_end); + + // RH TODO: link this to rippled's internal STObject constants + // E.g.: + /* + int field_code = (safe_cast(type) << 16) | field; + auto const& fieldObj = ripple::SField::getField; + */ + + if (type < 1 || type > 19 || (type >= 9 && type <= 13)) + return Unexpected(pe_unknown_type_early); + + bool is_vl = + (type == SerializedTypeID::STI_ACCOUNT || + type == SerializedTypeID::STI_VL || + type == SerializedTypeID::STI_PATHSET || + type == SerializedTypeID::STI_VECTOR256); + + int length = -1; + if (is_vl) + { + length = *upto++; + if (upto >= end) + return Unexpected(pe_unexpected_end); + + if (length < 193) + { + // do nothing + } + else if (length > 192 && length < 241) + { + length -= 193; + length *= 256; + length += *upto++ + 193; + if (upto > end) + return Unexpected(pe_unexpected_end); + } + else + { + int b2 = *upto++; + if (upto >= end) + return Unexpected(pe_unexpected_end); + length -= 241; + length *= 65536; + length += 12481 + (b2 * 256) + *upto++; + if (upto >= end) + return Unexpected(pe_unexpected_end); + } + } + else if ((type >= 1 && type <= 5) || type == 16 || type == 17) + { + switch (type) + { + case SerializedTypeID::STI_UINT16: + length = 2; + break; + case SerializedTypeID::STI_UINT32: + length = 4; + break; + case SerializedTypeID::STI_UINT64: + length = 8; + break; + case SerializedTypeID::STI_UINT128: + length = 16; + break; + case SerializedTypeID::STI_UINT256: + length = 32; + break; + case SerializedTypeID::STI_UINT8: + length = 1; + break; + case SerializedTypeID::STI_UINT160: + length = 20; + break; + default: + length = -1; + break; + } + } + else if (type == SerializedTypeID::STI_AMOUNT) + { + length = (*upto >> 6 == 1) ? 8 : 48; + if (upto >= end) + return Unexpected(pe_unexpected_end); + } + + if (length > -1) + { + payload_start = upto - start; + payload_length = length; + DBG_PRINTF( + "%d get_stobject_length field: %d Type: %d VL: %s Len: %d " + "Payload_Start: %d Payload_Len: %d\n", + recursion_depth, + field, + type, + (is_vl ? "yes" : "no"), + length, + payload_start, + payload_length); + return length + (upto - start); + } + + if (type == SerializedTypeID::STI_OBJECT || + type == SerializedTypeID::STI_ARRAY) + { + payload_start = upto - start; + + for (int i = 0; i < 1024; ++i) + { + int subfield = -1, subtype = -1, payload_start_ = -1, + payload_length_ = -1; + auto const sublength = get_stobject_length( + upto, + end, + subtype, + subfield, + payload_start_, + payload_length_, + recursion_depth + 1); + DBG_PRINTF( + "%d get_stobject_length i %d %d-%d, upto %d sublength %d\n", + recursion_depth, + i, + subtype, + subfield, + upto - start, + sublength); + if (!sublength) + return Unexpected(pe_unexpected_end); + upto += sublength.value(); + if (upto >= end) + return Unexpected(pe_unexpected_end); + + if ((*upto == 0xE1U && type == 0xEU) || + (*upto == 0xF1U && type == 0xFU)) + { + payload_length = upto - start - payload_start; + upto++; + return (upto - start); + } + } + return Unexpected(pe_excessive_size); + } + + return Unexpected(pe_unknown_type_late); +}; + +} // namespace hook diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index 50da50102..26496f6e5 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -1,6 +1,6 @@ +#include #include #include -#include #include #include #include @@ -521,29 +521,6 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv) namespace hook_float { -// power of 10 LUT for fast integer math -static int64_t power_of_ten[19] = { - 1LL, - 10LL, - 100LL, - 1000LL, - 10000LL, - 100000LL, - 1000000LL, - 10000000LL, - 100000000LL, - 1000000000LL, - 10000000000LL, - 100000000000LL, - 1000000000000LL, - 10000000000000LL, - 100000000000000LL, - 1000000000000000LL, // 15 - 10000000000000000LL, - 100000000000000000LL, - 1000000000000000000LL, -}; - using namespace hook_api; static int64_t const minMantissa = 1000000000000000ull; static int64_t const maxMantissa = 9999999999999999ull; @@ -656,121 +633,10 @@ make_float(uint64_t mantissa, int32_t exponent, bool neg) return out; } -/** - * This function normalizes the mantissa and exponent passed, if it can. - * It returns the XFL and mutates the supplied manitssa and exponent. - * If a negative mantissa is provided then the returned XFL has the negative - * flag set. If there is an overflow error return XFL_OVERFLOW. On underflow - * returns canonical 0 - */ -template -inline int64_t -normalize_xfl(T& man, int32_t& exp, bool neg = false) -{ - if (man == 0) - return 0; - - if (man == std::numeric_limits::min()) - man++; - - constexpr bool sman = std::is_same::value; - static_assert(sman || std::is_same()); - - if constexpr (sman) - { - if (man < 0) - { - man *= -1LL; - neg = true; - } - } - - // mantissa order - std::feclearexcept(FE_ALL_EXCEPT); - int32_t mo = log10(man); - // defensively ensure log10 produces a sane result; we'll borrow the - // overflow error code if it didn't - if (std::fetestexcept(FE_INVALID)) - return XFL_OVERFLOW; - - int32_t adjust = 15 - mo; - - if (adjust > 0) - { - // defensive check - if (adjust > 18) - return 0; - man *= power_of_ten[adjust]; - exp -= adjust; - } - else if (adjust < 0) - { - // defensive check - if (-adjust > 18) - return XFL_OVERFLOW; - man /= power_of_ten[-adjust]; - exp -= adjust; - } - - if (man == 0) - { - exp = 0; - return 0; - } - - // even after adjustment the mantissa can be outside the range by one place - // improving the math above would probably alleviate the need for these - // branches - if (man < minMantissa) - { - if (man == minMantissa - 1LL) - man += 1LL; - else - { - man *= 10LL; - exp--; - } - } - - if (man > maxMantissa) - { - if (man == maxMantissa + 1LL) - man -= 1LL; - else - { - man /= 10LL; - exp++; - } - } - - if (exp < minExponent) - { - man = 0; - exp = 0; - return 0; - } - - if (man == 0) - { - exp = 0; - return 0; - } - - if (exp > maxExponent) - return XFL_OVERFLOW; - - int64_t ret = make_float((uint64_t)man, exp, neg); - if constexpr (sman) - { - if (neg) - man *= -1LL; - } - - return ret; -} - } // namespace hook_float using namespace hook_float; +using hook::Bytes; + inline int32_t no_free_slots(hook::HookContext& hookCtx) { @@ -1242,7 +1108,6 @@ hook::apply( .hookHash = hookHash, .hookCanEmit = hookCanEmit, .accountKeylet = keylet::account(account), - .ownerDirKeylet = keylet::ownerDir(account), .hookKeylet = keylet::hook(account), .account = account, .otxnAccount = applyCtx.tx.getAccountID(sfAccount), @@ -1438,187 +1303,6 @@ std::optional inline make_state_key(std::string_view source) return ripple::uint256::fromVoid(key_buffer); } -// check the state cache -inline std::optional< - std::reference_wrapper const>> -lookup_state_cache( - hook::HookContext& hookCtx, - ripple::AccountID const& acc, - ripple::uint256 const& ns, - ripple::uint256 const& key) -{ - auto& stateMap = hookCtx.result.stateMap; - if (stateMap.find(acc) == stateMap.end()) - return std::nullopt; - - auto& stateMapAcc = std::get<3>(stateMap[acc]); - if (stateMapAcc.find(ns) == stateMapAcc.end()) - return std::nullopt; - - auto& stateMapNs = stateMapAcc[ns]; - - auto const& ret = stateMapNs.find(key); - - if (ret == stateMapNs.end()) - return std::nullopt; - - return std::cref(ret->second); -} - -// update the state cache -inline int64_t // if negative a hook return code, if == 1 then success -set_state_cache( - hook::HookContext& hookCtx, - ripple::AccountID const& acc, - ripple::uint256 const& ns, - ripple::uint256 const& key, - ripple::Blob& data, - bool modified) -{ - auto& stateMap = hookCtx.result.stateMap; - auto& view = hookCtx.applyCtx.view(); - - if (modified && stateMap.modified_entry_count >= max_state_modifications) - return TOO_MANY_STATE_MODIFICATIONS; - - bool const createNamespace = view.rules().enabled(fixXahauV1) && - !view.exists(keylet::hookStateDir(acc, ns)); - - if (stateMap.find(acc) == stateMap.end()) - { - // new Account Key - // if this is the first time this account has been interacted with - // we will compute how many available reserve positions there are - auto const& fees = hookCtx.applyCtx.view().fees(); - - auto const accSLE = view.read(ripple::keylet::account(acc)); - - if (!accSLE) - return DOESNT_EXIST; - - STAmount bal = accSLE->getFieldAmount(sfBalance); - - uint16_t const hookStateScale = accSLE->isFieldPresent(sfHookStateScale) - ? accSLE->getFieldU16(sfHookStateScale) - : 1; - - int64_t availableForReserves = bal.xrp().drops() - - fees.accountReserve(accSLE->getFieldU32(sfOwnerCount)).drops(); - - int64_t increment = fees.increment.drops(); - - if (increment <= 0) - increment = 1; - - availableForReserves /= increment; - - if (availableForReserves < hookStateScale && modified) - return RESERVE_INSUFFICIENT; - - int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces) - ? accSLE->getFieldV256(sfHookNamespaces).size() - : 0; - - if (createNamespace) - { - // overflow should never ever happen but check anyway - if (namespaceCount + 1 < namespaceCount) - return INTERNAL_ERROR; - - if (++namespaceCount > hook::maxNamespaces()) - return TOO_MANY_NAMESPACES; - } - - stateMap.modified_entry_count++; - - // sanity check - if (view.rules().enabled(featureExtendedHookState) && - availableForReserves < hookStateScale) - return INTERNAL_ERROR; - - stateMap[acc] = { - availableForReserves - hookStateScale, - namespaceCount, - hookStateScale, - {{ns, {{key, {modified, data}}}}}}; - return 1; - } - - auto& availableForReserves = std::get<0>(stateMap[acc]); - auto& namespaceCount = std::get<1>(stateMap[acc]); - auto& hookStateScale = std::get<2>(stateMap[acc]); - auto& stateMapAcc = std::get<3>(stateMap[acc]); - bool const canReserveNew = availableForReserves >= hookStateScale; - - if (stateMapAcc.find(ns) == stateMapAcc.end()) - { - // new Namespace Key - if (modified) - { - if (!canReserveNew) - return RESERVE_INSUFFICIENT; - - if (createNamespace) - { - // overflow should never ever happen but check anyway - if (namespaceCount + 1 < namespaceCount) - return INTERNAL_ERROR; - - if (namespaceCount + 1 > hook::maxNamespaces()) - return TOO_MANY_NAMESPACES; - - namespaceCount++; - } - - if (view.rules().enabled(featureExtendedHookState) && - availableForReserves < hookStateScale) - return INTERNAL_ERROR; - - availableForReserves -= hookStateScale; - stateMap.modified_entry_count++; - } - - stateMapAcc[ns] = {{key, {modified, data}}}; - - return 1; - } - - auto& stateMapNs = stateMapAcc[ns]; - if (stateMapNs.find(key) == stateMapNs.end()) - { - // new State Key - if (modified) - { - if (!canReserveNew) - return RESERVE_INSUFFICIENT; - - if (view.rules().enabled(featureExtendedHookState) && - availableForReserves < hookStateScale) - return INTERNAL_ERROR; - - availableForReserves -= hookStateScale; - stateMap.modified_entry_count++; - } - - stateMapNs[key] = {modified, data}; - hookCtx.result.changedStateCount++; - return 1; - } - - // existing State Key - if (modified) - { - if (!stateMapNs[key].first) - hookCtx.result.changedStateCount++; - - stateMap.modified_entry_count++; - stateMapNs[key].first = true; - } - - stateMapNs[key].second = data; - return 1; -} - DEFINE_HOOK_FUNCTION( int64_t, state_set, @@ -1706,9 +1390,8 @@ DEFINE_HOOK_FUNCTION( if (read_len > maxSize) return TOO_BIG; - uint256 ns = nread_len == 0 - ? hookCtx.result.hookNamespace - : ripple::base_uint<256>::fromVoid(memory + nread_ptr); + uint256 ns = nread_len == 0 ? hookCtx.result.hookNamespace + : uint256::fromVoid(memory + nread_ptr); ripple::AccountID acc = aread_len == 20 ? AccountID::fromVoid(memory + aread_ptr) @@ -1729,107 +1412,11 @@ DEFINE_HOOK_FUNCTION( ripple::Blob data{memory + read_ptr, memory + read_ptr + read_len}; - // local modifications are always allowed - if (aread_len == 0 || acc == hookCtx.result.account) - { - if (int64_t ret = set_state_cache(hookCtx, acc, ns, *key, data, true); - ret < 0) - return ret; + auto const result = api.state_foreign_set(*key, ns, acc, data); + if (!result) + return result.error(); + return result.value(); - return read_len; - } - - // execution to here means it's actually a foreign set - if (hookCtx.result.foreignStateSetDisabled) - return PREVIOUS_FAILURE_PREVENTS_RETRY; - - // first check if we've already modified this state - auto cacheEntry = lookup_state_cache(hookCtx, acc, ns, *key); - if (cacheEntry && cacheEntry->get().first) - { - // if a cache entry already exists and it has already been modified - // don't check grants again - if (int64_t ret = set_state_cache(hookCtx, acc, ns, *key, data, true); - ret < 0) - return ret; - - return read_len; - } - - // cache miss or cache was present but entry was not marked as previously - // modified therefore before continuing we need to check grants - auto const sle = view.read(ripple::keylet::hook(acc)); - if (!sle) - return INTERNAL_ERROR; - - bool found_auth = false; - - // we do this by iterating the hooks installed on the foreign account and in - // turn their grants and namespaces - auto const& hooks = sle->getFieldArray(sfHooks); - for (auto const& hookObj : hooks) - { - // skip blank entries - if (!hookObj.isFieldPresent(sfHookHash)) - continue; - - if (!hookObj.isFieldPresent(sfHookGrants)) - continue; - - auto const& hookGrants = hookObj.getFieldArray(sfHookGrants); - - if (hookGrants.size() < 1) - continue; - - // the grant allows the hook to modify the granter's namespace only - if (hookObj.isFieldPresent(sfHookNamespace)) - { - if (hookObj.getFieldH256(sfHookNamespace) != ns) - continue; - } - else - { - // fetch the hook definition - auto const def = view.read(ripple::keylet::hookDefinition( - hookObj.getFieldH256(sfHookHash))); - if (!def) // should never happen except in a rare race condition - continue; - if (def->getFieldH256(sfHookNamespace) != ns) - continue; - } - - // this is expensive search so we'll disallow after one failed attempt - for (auto const& hookGrantObj : hookGrants) - { - bool hasAuthorizedField = hookGrantObj.isFieldPresent(sfAuthorize); - - if (hookGrantObj.getFieldH256(sfHookHash) == - hookCtx.result.hookHash && - (!hasAuthorizedField || - hookGrantObj.getAccountID(sfAuthorize) == - hookCtx.result.account)) - { - found_auth = true; - break; - } - } - - if (found_auth) - break; - } - - if (!found_auth) - { - // hook only gets one attempt - hookCtx.result.foreignStateSetDisabled = true; - return NOT_AUTHORIZED; - } - - if (int64_t ret = set_state_cache(hookCtx, acc, ns, *key, data, true); - ret < 0) - return ret; - - return read_len; HOOK_TEARDOWN(); } @@ -2168,9 +1755,8 @@ DEFINE_HOOK_FUNCTION( NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) return OUT_OF_BOUNDS; - uint256 ns = nread_len == 0 - ? hookCtx.result.hookNamespace - : ripple::base_uint<256>::fromVoid(memory + nread_ptr); + uint256 ns = nread_len == 0 ? hookCtx.result.hookNamespace + : uint256::fromVoid(memory + nread_ptr); ripple::AccountID acc = is_foreign ? AccountID::fromVoid(memory + aread_ptr) : hookCtx.result.account; @@ -2181,30 +1767,10 @@ DEFINE_HOOK_FUNCTION( if (!key) return INVALID_ARGUMENT; - // first check if the requested state was previously cached this session - auto cacheEntryLookup = lookup_state_cache(hookCtx, acc, ns, *key); - if (cacheEntryLookup) - { - auto const& cacheEntry = cacheEntryLookup->get(); - - WRITE_WASM_MEMORY_OR_RETURN_AS_INT64( - write_ptr, - write_len, - cacheEntry.second.data(), - cacheEntry.second.size(), - false); - } - - auto hsSLE = view.peek(keylet::hookState(acc, *key, ns)); - - if (!hsSLE) - return DOESNT_EXIST; - - Blob b = hsSLE->getFieldVL(sfHookStateData); - - // it exists add it to cache and return it - if (set_state_cache(hookCtx, acc, ns, *key, b, false) < 0) - return INTERNAL_ERROR; // should never happen + auto const result = api.state_foreign(*key, ns, acc); + if (!result) + return result.error(); + auto const& b = result.value(); WRITE_WASM_MEMORY_OR_RETURN_AS_INT64( write_ptr, write_len, b.data(), b.size(), false); @@ -2251,10 +1817,11 @@ DEFINE_HOOK_FUNCTION( HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - auto const& txID = - (hookCtx.emitFailure && !flags - ? applyCtx.tx.getFieldH256(sfTransactionHash) - : applyCtx.tx.getTransactionID()); + auto const result = api.otxn_id(flags); + if (!result) + return result.error(); + + auto const& txID = result.value(); if (txID.size() > write_len) return TOO_SMALL; @@ -2280,11 +1847,7 @@ DEFINE_HOOK_FUNCTION(int64_t, otxn_type) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.emitFailure) - return safe_cast( - hookCtx.emitFailure->getFieldU16(sfTransactionType)); - - return applyCtx.tx.getTxnType(); + return api.otxn_type(); HOOK_TEARDOWN(); } @@ -2294,31 +1857,11 @@ DEFINE_HOOK_FUNCTION(int64_t, otxn_slot, uint32_t slot_into) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (slot_into > hook_api::max_slots) - return INVALID_ARGUMENT; + auto const result = api.otxn_slot(slot_into); + if (!result) + return result.error(); - // check if we can emplace the object to a slot - if (slot_into == 0 && no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - if (slot_into == 0) - { - if (auto found = get_free_slot(hookCtx); found) - slot_into = *found; - else - return NO_FREE_SLOTS; - } - - auto const& st_tx = std::make_shared( - hookCtx.emitFailure ? *(hookCtx.emitFailure) - : const_cast(applyCtx.tx) - .downcast()); - - hookCtx.slot[slot_into] = hook::SlotEntry{.storage = st_tx, .entry = 0}; - - hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); - - return slot_into; + return result.value(); HOOK_TEARDOWN(); } @@ -2327,35 +1870,8 @@ DEFINE_HOOK_FUNCTION(int64_t, otxn_slot, uint32_t slot_into) // hook invocation DEFINE_HOOK_FUNCTION(int64_t, otxn_burden) { - HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, - // hookCtx on current stack - - if (hookCtx.burden) - return hookCtx.burden; - - auto const& tx = applyCtx.tx; - if (!tx.isFieldPresent(sfEmitDetails)) - return 1; // burden is always 1 if the tx wasn't a emit - - auto const& pd = const_cast(tx) - .getField(sfEmitDetails) - .downcast(); - - if (!pd.isFieldPresent(sfEmitBurden)) - { - JLOG(j.warn()) - << "HookError[" << HC_ACC() - << "]: found sfEmitDetails but sfEmitBurden was not present"; - return 1; - } - - uint64_t burden = pd.getFieldU64(sfEmitBurden); - burden &= - ((1ULL << 63) - - 1); // wipe out the two high bits just in case somehow they are set - hookCtx.burden = burden; - return (int64_t)(burden); - + HOOK_SETUP(); + return api.otxn_burden(); HOOK_TEARDOWN(); } @@ -2364,32 +1880,8 @@ DEFINE_HOOK_FUNCTION(int64_t, otxn_burden) // hook invocation DEFINE_HOOK_FUNCTION(int64_t, otxn_generation) { - HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, - // hookCtx on current stack - - // cache the result as it will not change for this hook execution - if (hookCtx.generation) - return hookCtx.generation; - - auto const& tx = applyCtx.tx; - if (!tx.isFieldPresent(sfEmitDetails)) - return 0; // generation is always 0 if the tx wasn't a emit - - auto const& pd = const_cast(tx) - .getField(sfEmitDetails) - .downcast(); - - if (!pd.isFieldPresent(sfEmitGeneration)) - { - JLOG(j.warn()) - << "HookError[" << HC_ACC() - << "]: found sfEmitDetails but sfEmitGeneration was not present"; - return 0; - } - - hookCtx.generation = pd.getFieldU32(sfEmitGeneration); - return hookCtx.generation; - + HOOK_SETUP(); + return api.otxn_generation(); HOOK_TEARDOWN(); } @@ -2397,7 +1889,7 @@ DEFINE_HOOK_FUNCTION(int64_t, otxn_generation) DEFINE_HOOK_FUNCTION(int64_t, etxn_generation) { // proxy only, no setup or teardown - return otxn_generation(hookCtx, frameCtx) + 1; + return hookCtx.api().etxn_generation(); } // Return the current ledger sequence number @@ -2405,7 +1897,7 @@ DEFINE_HOOK_FUNCTION(int64_t, ledger_seq) { HOOK_SETUP(); - return view.info().seq; + return api.ledger_seq(); HOOK_TEARDOWN(); } @@ -2423,7 +1915,7 @@ DEFINE_HOOK_FUNCTION( if (write_len < 32) return TOO_SMALL; - uint256 hash = view.info().parentHash; + auto const hash = api.ledger_last_hash(); WRITE_WASM_MEMORY_AND_RETURN( write_ptr, write_len, hash.data(), 32, memory, memory_length); @@ -2435,9 +1927,7 @@ DEFINE_HOOK_FUNCTION(int64_t, ledger_last_time) { HOOK_SETUP(); - return std::chrono::duration_cast( - view.info().parentCloseTime.time_since_epoch()) - .count(); + return api.ledger_last_time(); HOOK_TEARDOWN(); } @@ -2463,27 +1953,21 @@ DEFINE_HOOK_FUNCTION( else if NOT_IN_BOUNDS (write_ptr, write_len, memory_length) return OUT_OF_BOUNDS; - SField const& fieldType = ripple::SField::getField(field_id); + auto const result = api.otxn_field(field_id); + if (!result) + return result.error(); - if (fieldType == sfInvalid) - return INVALID_FIELD; - - if (!applyCtx.tx.isFieldPresent(fieldType)) - return DOESNT_EXIST; - - auto const& field = hookCtx.emitFailure - ? hookCtx.emitFailure->getField(fieldType) - : const_cast(applyCtx.tx).getField(fieldType); + auto const& field = result.value(); Serializer s; - field.add(s); + field->add(s); WRITE_WASM_MEMORY_OR_RETURN_AS_INT64( write_ptr, write_len, s.getDataPtr(), s.getDataLength(), - field.getSType() == STI_ACCOUNT); + field->getSType() == STI_ACCOUNT); HOOK_TEARDOWN(); } @@ -2513,21 +1997,19 @@ DEFINE_HOOK_FUNCTION( return TOO_SMALL; } - if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) - return DOESNT_EXIST; - - if (hookCtx.slot[slot_no].entry == 0) - return INTERNAL_ERROR; + auto const result = api.slot(slot_no); + if (!result) + return result.error(); Serializer s; - hookCtx.slot[slot_no].entry->add(s); + (*result)->add(s); WRITE_WASM_MEMORY_OR_RETURN_AS_INT64( write_ptr, write_len, s.getDataPtr(), s.getDataLength(), - hookCtx.slot[slot_no].entry->getSType() == STI_ACCOUNT); + (*result)->getSType() == STI_ACCOUNT); HOOK_TEARDOWN(); } @@ -2537,13 +2019,11 @@ DEFINE_HOOK_FUNCTION(int64_t, slot_clear, uint32_t slot_no) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_clear(slot_no); + if (!result) + return result.error(); - hookCtx.slot.erase(slot_no); - hookCtx.slot_free.push(slot_no); - - return 1; + return result.value(); HOOK_TEARDOWN(); } @@ -2553,16 +2033,11 @@ DEFINE_HOOK_FUNCTION(int64_t, slot_count, uint32_t slot_no) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_count(slot_no); + if (!result) + return result.error(); - if (hookCtx.slot[slot_no].entry == 0) - return INTERNAL_ERROR; - - if (hookCtx.slot[slot_no].entry->getSType() != STI_ARRAY) - return NOT_AN_ARRAY; - - return hookCtx.slot[slot_no].entry->downcast().size(); + return result.value(); HOOK_TEARDOWN(); } @@ -2580,68 +2055,12 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - if ((read_len != 32 && read_len != 34) || slot_into > hook_api::max_slots) - return INVALID_ARGUMENT; + Bytes data{memory + read_ptr, memory + read_ptr + read_len}; + auto const result = api.slot_set(data, slot_into); + if (!result) + return result.error(); - // check if we can emplace the object to a slot - if (slot_into == 0 && no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - std::vector slot_key{ - memory + read_ptr, memory + read_ptr + read_len}; - std::optional> slot_value = - std::nullopt; - - if (read_len == 34) - { - std::optional kl = - unserialize_keylet(memory + read_ptr, read_len); - if (!kl) - return DOESNT_EXIST; - - if (kl->key == beast::zero) - return DOESNT_EXIST; - - auto const sle = applyCtx.view().read(*kl); - if (!sle) - return DOESNT_EXIST; - - slot_value = sle; - } - else if (read_len == 32) - { - uint256 hash = ripple::base_uint<256>::fromVoid(memory + read_ptr); - - ripple::error_code_i ec{ripple::error_code_i::rpcUNKNOWN}; - - auto hTx = applyCtx.app.getMasterTransaction().fetch(hash, ec); - - if (auto const* p = std::get_if, - std::shared_ptr>>(&hTx)) - slot_value = p->first->getSTransaction(); - else - return DOESNT_EXIST; - } - else - return DOESNT_EXIST; - - if (!slot_value.has_value()) - return DOESNT_EXIST; - - if (slot_into == 0) - { - if (auto found = get_free_slot(hookCtx); found) - slot_into = *found; - else - return NO_FREE_SLOTS; - } - - hookCtx.slot[slot_into] = - hook::SlotEntry{.storage = *slot_value, .entry = 0}; - hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); - - return slot_into; + return result.value(); HOOK_TEARDOWN(); } @@ -2651,16 +2070,11 @@ DEFINE_HOOK_FUNCTION(int64_t, slot_size, uint32_t slot_no) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_size(slot_no); + if (!result) + return result.error(); - if (hookCtx.slot[slot_no].entry == 0) - return INTERNAL_ERROR; - - // RH TODO: this is a very expensive way of computing size, cache it - Serializer s; - hookCtx.slot[slot_no].entry->add(s); - return s.getDataLength(); + return result.value(); HOOK_TEARDOWN(); } @@ -2675,57 +2089,11 @@ DEFINE_HOOK_FUNCTION( HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(parent_slot) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_subarray(parent_slot, array_id, new_slot); + if (!result) + return result.error(); - if (hookCtx.slot[parent_slot].entry == 0) - return INTERNAL_ERROR; - - if (hookCtx.slot[parent_slot].entry->getSType() != STI_ARRAY) - return NOT_AN_ARRAY; - - if (new_slot == 0 && no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - if (new_slot > hook_api::max_slots) - return INVALID_ARGUMENT; - - bool copied = false; - try - { - ripple::STArray& parent_obj = - const_cast(*hookCtx.slot[parent_slot].entry) - .downcast(); - - if (parent_obj.size() <= array_id) - return DOESNT_EXIST; - - if (new_slot == 0) - { - if (auto found = get_free_slot(hookCtx); found) - new_slot = *found; - else - return NO_FREE_SLOTS; - } - - // copy - if (new_slot != parent_slot) - { - copied = true; - hookCtx.slot[new_slot] = hookCtx.slot[parent_slot]; - } - hookCtx.slot[new_slot].entry = &(parent_obj[array_id]); - return new_slot; - } - catch (const std::bad_cast& e) - { - if (copied) - { - hookCtx.slot.erase(new_slot); - hookCtx.slot_free.push(new_slot); - } - return NOT_AN_ARRAY; - } + return result.value(); HOOK_TEARDOWN(); } @@ -2740,61 +2108,11 @@ DEFINE_HOOK_FUNCTION( HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(parent_slot) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_subfield(parent_slot, field_id, new_slot); + if (!result) + return result.error(); - if (new_slot == 0 && no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - if (new_slot > hook_api::max_slots) - return INVALID_ARGUMENT; - - SField const& fieldCode = ripple::SField::getField(field_id); - - if (fieldCode == sfInvalid) - return INVALID_FIELD; - - if (hookCtx.slot[parent_slot].entry == 0) - return INTERNAL_ERROR; - - bool copied = false; - - try - { - ripple::STObject& parent_obj = - const_cast(*hookCtx.slot[parent_slot].entry) - .downcast(); - - if (!parent_obj.isFieldPresent(fieldCode)) - return DOESNT_EXIST; - - if (new_slot == 0) - { - if (auto found = get_free_slot(hookCtx); found) - new_slot = *found; - else - return NO_FREE_SLOTS; - } - - // copy - if (new_slot != parent_slot) - { - copied = true; - hookCtx.slot[new_slot] = hookCtx.slot[parent_slot]; - } - - hookCtx.slot[new_slot].entry = &(parent_obj.getField(fieldCode)); - return new_slot; - } - catch (const std::bad_cast& e) - { - if (copied) - { - hookCtx.slot.erase(new_slot); - hookCtx.slot_free.push(new_slot); - } - return NOT_AN_OBJECT; - } + return result.value(); HOOK_TEARDOWN(); } @@ -2804,34 +2122,19 @@ DEFINE_HOOK_FUNCTION(int64_t, slot_type, uint32_t slot_no, uint32_t flags) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_type(slot_no, flags); + if (!result) + return result.error(); - if (hookCtx.slot[slot_no].entry == 0) - return INTERNAL_ERROR; - try + if (flags == 0) { - ripple::STBase& obj = const_cast( - *hookCtx.slot[slot_no].entry); //.downcast(); - if (flags == 0) - return obj.getFName().fieldCode; - - // this flag is for use with an amount field to determine if the amount - // is native (xrp) - if (flags == 1) - { - if (obj.getSType() != STI_AMOUNT) - return NOT_AN_AMOUNT; - return const_cast(*hookCtx.slot[slot_no].entry) - .downcast() - .native(); - } - - return INVALID_ARGUMENT; + auto const base = std::get<0>(*result); + return base.getFName().fieldCode; } - catch (const std::bad_cast& e) + else { - return INTERNAL_ERROR; + auto const amount = std::get<1>(*result); + return amount.native(); } HOOK_TEARDOWN(); @@ -2842,42 +2145,11 @@ DEFINE_HOOK_FUNCTION(int64_t, slot_float, uint32_t slot_no) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) - return DOESNT_EXIST; + auto const result = api.slot_float(slot_no); + if (!result) + return result.error(); - if (hookCtx.slot[slot_no].entry == 0) - return INTERNAL_ERROR; - - try - { - ripple::STAmount& st_amt = - const_cast(*hookCtx.slot[slot_no].entry) - .downcast(); - - int64_t normalized = 0; - if (st_amt.native()) - { - ripple::XRPAmount amt = st_amt.xrp(); - int64_t drops = amt.drops(); - int32_t exp = -6; - // normalize - normalized = hook_float::normalize_xfl(drops, exp); - } - else - { - ripple::IOUAmount amt = st_amt.iou(); - normalized = make_float(amt); - } - - if (normalized == - EXPONENT_UNDERSIZED /* exponent undersized (underflow) */) - return 0; // return 0 in this case - return normalized; - } - catch (const std::bad_cast& e) - { - return NOT_AN_AMOUNT; - } + return result.value(); HOOK_TEARDOWN(); } @@ -2958,8 +2230,7 @@ DEFINE_HOOK_FUNCTION( if (read_len != 32) return INVALID_ARGUMENT; - base_uint<256> id = - ripple::base_uint<256>::fromVoid(memory + read_ptr); + uint256 id = uint256::fromVoid(memory + read_ptr); ripple::Keylet kl = keylet_type == keylet_code::CHILD ? ripple::keylet::child(id) @@ -3092,8 +2363,8 @@ DEFINE_HOOK_FUNCTION( return INVALID_ARGUMENT; uint64_t index = (((uint64_t)c) << 32U) + ((uint64_t)d); - ripple::Keylet kl = ripple::keylet::page( - ripple::base_uint<256>::fromVoid(memory + a), index); + ripple::Keylet kl = + ripple::keylet::page(uint256::fromVoid(memory + a), index); return serialize_keylet(kl, memory, write_ptr, write_len); } @@ -3115,8 +2386,8 @@ DEFINE_HOOK_FUNCTION( ripple::Keylet kl = ripple::keylet::hookState( AccountID::fromVoid(memory + aread_ptr), - ripple::base_uint<256>::fromVoid(memory + kread_ptr), - ripple::base_uint<256>::fromVoid(memory + nread_ptr)); + uint256::fromVoid(memory + kread_ptr), + uint256::fromVoid(memory + nread_ptr)); return serialize_keylet(kl, memory, write_ptr, write_len); } @@ -3143,7 +2414,7 @@ DEFINE_HOOK_FUNCTION( ripple::Keylet kl = ripple::keylet::hookStateDir( AccountID::fromVoid(memory + aread_ptr), - ripple::base_uint<256>::fromVoid(memory + nread_ptr)); + uint256::fromVoid(memory + nread_ptr)); return serialize_keylet(kl, memory, write_ptr, write_len); } @@ -3327,317 +2598,16 @@ DEFINE_HOOK_FUNCTION( if (write_len < 32) return TOO_SMALL; - auto& app = hookCtx.applyCtx.app; + // Delegate to decoupled HookAPI for emit logic + ripple::Slice txBlob{ + reinterpret_cast(memory + read_ptr), read_len}; - if (hookCtx.expected_etxn_count < 0) - return PREREQUISITE_NOT_MET; + auto const res = api.emit(txBlob); - if (hookCtx.result.emittedTxn.size() >= hookCtx.expected_etxn_count) - return TOO_MANY_EMITTED_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()) << "HookEmit[" << HC_ACC() << "]: Failed " << e.what() - << "\n"; - return EMISSION_FAILURE; - } - - if (isPseudoTx(*stpTrans)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: Attempted to emit pseudo txn."; - return EMISSION_FAILURE; - } - - ripple::TxType txType = stpTrans->getTxnType(); - - ripple::uint256 const& hookCanEmit = hookCtx.result.hookCanEmit; - if (!hook::canEmit(txType, hookCanEmit)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: Hook cannot emit this txn."; - return EMISSION_FAILURE; - } - - // check the emitted txn is valid - /* Emitted TXN rules - * 0. Account must match the hook account - * 1. Sequence: 0 - * 2. PubSigningKey: 000000000000000 - * 3. sfEmitDetails present and valid - * 4. No sfTxnSignature - * 5. LastLedgerSeq > current ledger, > firstledgerseq & LastLedgerSeq < seq - * + 5 - * 6. FirstLedgerSeq > current ledger - * 7. Fee must be correctly high - * 8. The generation cannot be higher than 10 - */ - - // rule 0: account must match the hook account - if (!stpTrans->isFieldPresent(sfAccount) || - stpTrans->getAccountID(sfAccount) != hookCtx.result.account) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfAccount does not match hook account"; - return EMISSION_FAILURE; - } - - // rule 1: sfSequence must be present and 0 - if (!stpTrans->isFieldPresent(sfSequence) || - stpTrans->getFieldU32(sfSequence) != 0) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfSequence missing or non-zero"; - return EMISSION_FAILURE; - } - - // rule 2: sfSigningPubKey must be present and 00...00 - if (!stpTrans->isFieldPresent(sfSigningPubKey)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfSigningPubKey missing"; - return EMISSION_FAILURE; - } - - auto const pk = stpTrans->getSigningPubKey(); - if (pk.size() != 33 && pk.size() != 0) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfSigningPubKey present but wrong size" - << " expecting 33 bytes"; - return EMISSION_FAILURE; - } - - for (int i = 0; i < pk.size(); ++i) - if (pk[i] != 0) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfSigningPubKey present but non-zero."; - return EMISSION_FAILURE; - } - - // rule 2.a: no signers - if (stpTrans->isFieldPresent(sfSigners)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfSigners not allowed in emitted txns."; - return EMISSION_FAILURE; - } - - // rule 2.b: ticketseq cannot be used - if (stpTrans->isFieldPresent(sfTicketSequence)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfTicketSequence not allowed in emitted txns."; - return EMISSION_FAILURE; - } - - // rule 2.c sfAccountTxnID not allowed - if (stpTrans->isFieldPresent(sfAccountTxnID)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfAccountTxnID not allowed in emitted txns."; - return EMISSION_FAILURE; - } - - // rule 3: sfEmitDetails must be present and valid - if (!stpTrans->isFieldPresent(sfEmitDetails)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitDetails missing."; - return EMISSION_FAILURE; - } - - auto const& emitDetails = const_cast(*stpTrans) - .getField(sfEmitDetails) - .downcast(); - - if (!emitDetails.isFieldPresent(sfEmitGeneration) || - !emitDetails.isFieldPresent(sfEmitBurden) || - !emitDetails.isFieldPresent(sfEmitParentTxnID) || - !emitDetails.isFieldPresent(sfEmitNonce) || - !emitDetails.isFieldPresent(sfEmitHookHash)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitDetails malformed."; - return EMISSION_FAILURE; - } - - // rule 8: emit generation cannot exceed 10 - if (emitDetails.getFieldU32(sfEmitGeneration) >= 10) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitGeneration was 10 or more."; - return EMISSION_FAILURE; - } - - uint32_t gen = emitDetails.getFieldU32(sfEmitGeneration); - uint64_t bur = emitDetails.getFieldU64(sfEmitBurden); - ripple::uint256 const& pTxnID = emitDetails.getFieldH256(sfEmitParentTxnID); - ripple::uint256 const& nonce = emitDetails.getFieldH256(sfEmitNonce); - - std::optional callback; - if (emitDetails.isFieldPresent(sfEmitCallback)) - callback = emitDetails.getAccountID(sfEmitCallback); - - auto const& hash = emitDetails.getFieldH256(sfEmitHookHash); - - uint32_t gen_proper = etxn_generation(hookCtx, frameCtx); - - if (gen != gen_proper) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitGeneration provided in EmitDetails " - << "not correct (" << gen << ") " - << "should be " << gen_proper; - return EMISSION_FAILURE; - } - - uint64_t bur_proper = etxn_burden(hookCtx, frameCtx); - if (bur != bur_proper) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitBurden provided in EmitDetails " - << "was not correct (" << bur << ") " - << "should be " << bur_proper; - return EMISSION_FAILURE; - } - - if (pTxnID != applyCtx.tx.getTransactionID()) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitParentTxnID provided in EmitDetails " - << "was not correct"; - return EMISSION_FAILURE; - } - - if (hookCtx.nonce_used.find(nonce) == hookCtx.nonce_used.end()) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitNonce provided in EmitDetails " - << "was not generated by nonce api"; - return EMISSION_FAILURE; - } - - if (callback && *callback != hookCtx.result.account) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfEmitCallback account must be the account " - << "of the emitting hook"; - return EMISSION_FAILURE; - } - - if (hash != hookCtx.result.hookHash) - { - JLOG(j.trace()) - << "HookEmit[" << HC_ACC() - << "]: sfEmitHookHash must be the hash of the emitting hook"; - return EMISSION_FAILURE; - } - - // rule 4: sfTxnSignature must be absent - if (stpTrans->isFieldPresent(sfTxnSignature)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfTxnSignature is present but should not be"; - return EMISSION_FAILURE; - } - - // rule 5: LastLedgerSeq must be present and after current ledger - if (!stpTrans->isFieldPresent(sfLastLedgerSequence)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfLastLedgerSequence missing"; - return EMISSION_FAILURE; - } - - uint32_t tx_lls = stpTrans->getFieldU32(sfLastLedgerSequence); - uint32_t ledgerSeq = view.info().seq; - if (tx_lls < ledgerSeq + 1) - { - JLOG(j.trace()) - << "HookEmit[" << HC_ACC() - << "]: sfLastLedgerSequence invalid (less than next ledger)"; - return EMISSION_FAILURE; - } - - if (tx_lls > ledgerSeq + 5) - { - JLOG(j.trace()) - << "HookEmit[" << HC_ACC() - << "]: sfLastLedgerSequence cannot be greater than current seq + 5"; - return EMISSION_FAILURE; - } - - // rule 6 - if (!stpTrans->isFieldPresent(sfFirstLedgerSequence) || - stpTrans->getFieldU32(sfFirstLedgerSequence) > tx_lls) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: sfFirstLedgerSequence must be present and " - << "<= LastLedgerSequence"; - return EMISSION_FAILURE; - } - - // rule 7 check the emitted txn pays the appropriate fee - int64_t minfee = etxn_fee_base(hookCtx, frameCtx, read_ptr, read_len); - - if (minfee < 0) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: Fee could not be calculated"; - return EMISSION_FAILURE; - } - - if (!stpTrans->isFieldPresent(sfFee)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: Fee missing from emitted tx"; - return EMISSION_FAILURE; - } - - int64_t fee = stpTrans->getFieldAmount(sfFee).xrp().drops(); - if (fee < minfee) - { - JLOG(j.trace()) - << "HookEmit[" << HC_ACC() - << "]: Fee on emitted txn is less than the minimum required fee"; - return EMISSION_FAILURE; - } - - std::string reason; - auto tpTrans = std::make_shared(stpTrans, reason, app); - if (tpTrans->getStatus() != NEW) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: tpTrans->getStatus() != NEW"; - return EMISSION_FAILURE; - } - - // preflight the transaction - auto preflightResult = ripple::preflight( - applyCtx.app, - applyCtx.view().rules(), - *stpTrans, - ripple::ApplyFlags::tapPREFLIGHT_EMIT, - j); - - if (!isTesSuccess(preflightResult.ter)) - { - JLOG(j.trace()) << "HookEmit[" << HC_ACC() - << "]: Transaction preflight failure: " - << transHuman(preflightResult.ter); - return EMISSION_FAILURE; - } + if (!res) + return res.error(); + auto const& tpTrans = *res; // 32 bytes auto const& txID = tpTrans->getID(); if (txID.size() > write_len) @@ -3662,6 +2632,7 @@ DEFINE_HOOK_FUNCTION( hookCtx.result.emittedTxn.push(tpTrans); return result; + HOOK_TEARDOWN(); } @@ -3682,31 +2653,10 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) return OUT_OF_BOUNDS; - if (hook_no == -1) - { - WRITE_WASM_MEMORY_AND_RETURN( - write_ptr, - write_len, - hookCtx.result.hookHash.data(), - 32, - memory, - memory_length); - } - - std::shared_ptr hookSLE = - applyCtx.view().peek(hookCtx.result.hookKeylet); - if (!hookSLE || !hookSLE->isFieldPresent(sfHooks)) - return INTERNAL_ERROR; - - ripple::STArray const& hooks = hookSLE->getFieldArray(sfHooks); - if (hook_no >= hooks.size()) - return DOESNT_EXIST; - - auto const& hook = hooks[hook_no]; - if (!hook.isFieldPresent(sfHookHash)) - return DOESNT_EXIST; - - ripple::uint256 const& hash = hook.getFieldH256(sfHookHash); + auto const result = api.hook_hash(hook_no); + if (!result) + return result.error(); + auto const& hash = result.value(); WRITE_WASM_MEMORY_AND_RETURN( write_ptr, write_len, hash.data(), hash.size(), memory, memory_length); @@ -3730,13 +2680,10 @@ DEFINE_HOOK_FUNCTION( if (ptr_len < 20) return TOO_SMALL; + auto const result = api.hook_account(); + WRITE_WASM_MEMORY_AND_RETURN( - write_ptr, - 20, - hookCtx.result.account.data(), - 20, - memory, - memory_length); + write_ptr, 20, result.data(), 20, memory, memory_length); HOOK_TEARDOWN(); } @@ -3755,28 +2702,18 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) return OUT_OF_BOUNDS; + // It is also checked in api.etxn_nonce, but for backwards compatibility, it + // must be checked before the TOO_SMALL check. if (hookCtx.emit_nonce_counter > hook_api::max_nonce) return TOO_MANY_NONCES; if (write_len < 32) return TOO_SMALL; - // in some cases the same hook might execute multiple times - // on one txn, therefore we need to pass this information to the nonce - uint32_t flags = 0; - flags |= hookCtx.result.isStrong ? 0b10U : 0; - flags |= hookCtx.result.isCallback ? 0b01U : 0; - flags |= (hookCtx.result.hookChainPosition << 2U); - - auto hash = ripple::sha512Half( - ripple::HashPrefix::emitTxnNonce, - applyCtx.tx.getTransactionID(), - hookCtx.emit_nonce_counter++, - hookCtx.result.account, - hookCtx.result.hookHash, - flags); - - hookCtx.nonce_used[hash] = true; + auto const result = api.etxn_nonce(); + if (!result) + return result.error(); + auto const& hash = result.value(); WRITE_WASM_MEMORY_AND_RETURN( write_ptr, 32, hash.data(), 32, memory, memory_length); @@ -3799,17 +2736,10 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) return OUT_OF_BOUNDS; - if (hookCtx.ledger_nonce_counter > hook_api::max_nonce) - return TOO_MANY_NONCES; - - auto hash = ripple::sha512Half( - ripple::HashPrefix::hookNonce, - view.info().seq, - view.info().parentCloseTime.time_since_epoch().count(), - view.info().parentHash, - applyCtx.tx.getTransactionID(), - hookCtx.ledger_nonce_counter++, - hookCtx.result.account); + auto const result = api.ledger_nonce(); + if (!result) + return result.error(); + auto const& hash = result.value(); WRITE_WASM_MEMORY_AND_RETURN( write_ptr, 32, hash.data(), 32, memory, memory_length); @@ -3849,17 +2779,10 @@ DEFINE_HOOK_FUNCTION( if (!klHi) return INVALID_ARGUMENT; - // keylets must be the same type! - if ((*klLo).type != (*klHi).type) - return DOES_NOT_MATCH; - - std::optional found = - view.succ((*klLo).key, (*klHi).key.next()); - - if (!found) - return DOESNT_EXIST; - - Keylet kl_out{(*klLo).type, *found}; + auto const result = api.ledger_keylet(*klLo, *klHi); + if (!result) + return result.error(); + auto kl_out = result.value(); return serialize_keylet(kl_out, memory, write_ptr, write_len); @@ -3872,18 +2795,10 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (hookCtx.expected_etxn_count > -1) - return ALREADY_SET; - - if (count < 1) - return TOO_SMALL; - - if (count > hook_api::max_emit) - return TOO_BIG; - - hookCtx.expected_etxn_count = count; - - return count; + auto const result = api.etxn_reserve(count); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -3891,22 +2806,11 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count) // Compute the burden of an emitted transaction based on a number of factors DEFINE_HOOK_FUNCTION(int64_t, etxn_burden) { - HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, - // hookCtx on current stack - - if (hookCtx.expected_etxn_count <= -1) - return PREREQUISITE_NOT_MET; - - uint64_t last_burden = (uint64_t)otxn_burden( - hookCtx, frameCtx); // always non-negative so cast is safe - - uint64_t burden = last_burden * hookCtx.expected_etxn_count; - if (burden < - last_burden) // this overflow will never happen but handle it anyway - return FEE_TOO_LARGE; - - return burden; - + HOOK_SETUP(); + auto const burden = api.etxn_burden(); + if (!burden) + return burden.error(); + return burden.value(); HOOK_TEARDOWN(); } @@ -3928,7 +2832,8 @@ DEFINE_HOOK_FUNCTION( NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - auto hash = ripple::sha512Half(ripple::Slice{memory + read_ptr, read_len}); + auto const hash = + api.util_sha512h(ripple::Slice{memory + read_ptr, read_len}); WRITE_WASM_MEMORY_AND_RETURN( write_ptr, 32, hash.data(), 32, memory, memory_length); @@ -3936,207 +2841,6 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } -// these are only used by get_stobject_length below -enum parse_error : int32_t { - pe_unexpected_end = -1, - pe_unknown_type_early = -2, // detected early - pe_unknown_type_late = -3, // end of function - pe_excessive_nesting = -4, - pe_excessive_size = -5 -}; - -// RH NOTE this is a light-weight stobject parsing function for drilling into a -// provided serialzied object however it could probably be replaced by an -// existing class or routine or set of routines in XRPLD Returns object length -// including header bytes (and footer bytes in the event of array or object) -// negative indicates error -inline int32_t -get_stobject_length( - unsigned char* start, // in - begin iterator - unsigned char* maxptr, // in - end iterator - int& type, // out - populated by serialized type code - int& field, // out - populated by serialized field code - int& payload_start, // out - the start of actual payload data for this type - int& payload_length, // out - the length of actual payload data for this - // type - int recursion_depth = 0) // used internally -{ - if (recursion_depth > 10) - return pe_excessive_nesting; - - unsigned char* end = maxptr; - unsigned char* upto = start; - int high = *upto >> 4; - int low = *upto & 0xF; - - upto++; - if (upto >= end) - return pe_unexpected_end; - if (high > 0 && low > 0) - { - // common type common field - type = high; - field = low; - } - else if (high > 0) - { - // common type, uncommon field - type = high; - field = *upto++; - } - else if (low > 0) - { - // common field, uncommon type - field = low; - type = *upto++; - } - else - { - // uncommon type and field - type = *upto++; - if (upto >= end) - return pe_unexpected_end; - field = *upto++; - } - - DBG_PRINTF( - "%d get_st_object found field %d type %d\n", - recursion_depth, - field, - type); - - if (upto >= end) - return pe_unexpected_end; - - // RH TODO: link this to rippled's internal STObject constants - // E.g.: - /* - int field_code = (safe_cast(type) << 16) | field; - auto const& fieldObj = ripple::SField::getField; - */ - - if (type < 1 || type > 19 || (type >= 9 && type <= 13)) - return pe_unknown_type_early; - - bool is_vl = (type == 8 /*ACCID*/ || type == 7 || type == 18 || type == 19); - - int length = -1; - if (is_vl) - { - length = *upto++; - if (upto >= end) - return pe_unexpected_end; - - if (length < 193) - { - // do nothing - } - else if (length > 192 && length < 241) - { - length -= 193; - length *= 256; - length += *upto++ + 193; - if (upto > end) - return pe_unexpected_end; - } - else - { - int b2 = *upto++; - if (upto >= end) - return pe_unexpected_end; - length -= 241; - length *= 65536; - length += 12481 + (b2 * 256) + *upto++; - if (upto >= end) - return pe_unexpected_end; - } - } - else if ((type >= 1 && type <= 5) || type == 16 || type == 17) - { - length = - (type == 1 - ? 2 - : (type == 2 - ? 4 - : (type == 3 - ? 8 - : (type == 4 - ? 16 - : (type == 5 - ? 32 - : (type == 16 - ? 1 - : (type == 17 ? 20 - : -1))))))); - } - else if (type == 6) /* AMOUNT */ - { - length = (*upto >> 6 == 1) ? 8 : 48; - if (upto >= end) - return pe_unexpected_end; - } - - if (length > -1) - { - payload_start = upto - start; - payload_length = length; - DBG_PRINTF( - "%d get_stobject_length field: %d Type: %d VL: %s Len: %d " - "Payload_Start: %d Payload_Len: %d\n", - recursion_depth, - field, - type, - (is_vl ? "yes" : "no"), - length, - payload_start, - payload_length); - return length + (upto - start); - } - - if (type == 15 || type == 14) /* Object / Array */ - { - payload_start = upto - start; - - for (int i = 0; i < 1024; ++i) - { - int subfield = -1, subtype = -1, payload_start_ = -1, - payload_length_ = -1; - int32_t sublength = get_stobject_length( - upto, - end, - subtype, - subfield, - payload_start_, - payload_length_, - recursion_depth + 1); - DBG_PRINTF( - "%d get_stobject_length i %d %d-%d, upto %d sublength %d\n", - recursion_depth, - i, - subtype, - subfield, - upto - start, - sublength); - if (sublength < 0) - return pe_unexpected_end; - upto += sublength; - if (upto >= end) - return pe_unexpected_end; - - if ((*upto == 0xE1U && type == 0xEU) || - (*upto == 0xF1U && type == 0xFU)) - { - payload_length = upto - start - payload_start; - upto++; - return (upto - start); - } - } - return pe_excessive_size; - } - - return pe_unknown_type_late; -} - // Given an serialized object in memory locate and return the offset and length // of the payload of a subfield of that object. Arrays are returned fully // formed. If successful returns offset and length joined as int64_t. Use @@ -4154,58 +2858,12 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - if (read_len < 2) - return TOO_SMALL; - - unsigned char* start = (unsigned char*)(memory + read_ptr); - unsigned char* upto = start; - unsigned char* end = start + read_len; - - DBG_PRINTF( - "sto_subfield called, looking for field %u type %u\n", - field_id & 0xFFFF, - (field_id >> 16)); - for (int j = -5; j < 5; ++j) - DBG_PRINTF((j == 0 ? " >%02X< " : " %02X "), *(start + j)); - DBG_PRINTF("\n"); - - // if ((*upto & 0xF0) == 0xE0) - // upto++; - - for (int i = 0; i < 1024 && upto < end; ++i) - { - int type = -1, field = -1, payload_start = -1, payload_length = -1; - int32_t length = get_stobject_length( - upto, end, type, field, payload_start, payload_length, 0); - if (length < 0) - return PARSE_ERROR; - if ((type << 16) + field == field_id) - { - DBG_PRINTF( - "sto_subfield returned for field %u type %u\n", - field_id & 0xFFFF, - (field_id >> 16)); - for (int j = -5; j < 5; ++j) - DBG_PRINTF((j == 0 ? " [%02X] " : " %02X "), *(upto + j)); - DBG_PRINTF("\n"); - - if (type == 0xF) // we return arrays fully formed - return (((int64_t)(upto - start)) - << 32) /* start of the object */ - + (uint32_t)(length); - - // return pointers to all other objects as payloads - return (((int64_t)(upto - start + payload_start)) - << 32U) /* start of the object */ - + (uint32_t)(payload_length); - } - upto += length; - } - - if (upto != end) - return PARSE_ERROR; - - return DOESNT_EXIST; + Bytes data{memory + read_ptr, memory + read_ptr + read_len}; + auto const result = api.sto_subfield(data, field_id); + if (!result) + return result.error(); + auto const& pair = result.value(); + return (uint64_t(pair.first) << 32U) + (uint32_t)pair.second; HOOK_TEARDOWN(); } @@ -4224,71 +2882,12 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - if (read_len < 2) - return TOO_SMALL; - - unsigned char* start = (unsigned char*)(memory + read_ptr); - unsigned char* upto = start; - unsigned char* end = start + read_len; - - // unwrap the array if it is wrapped, - // by removing a byte from the start and end - // why here 0xF0? - // STI_ARRAY = 0xF0 - // eg) Signers field value = 0x03 => 0xF3 - // eg) Amounts field value = 0x5C => 0xF0, 0x5C - if ((*upto & 0xF0U) == 0xF0U) - { - if (view.rules().enabled(fixHookAPI20251128) && *upto == 0xF0U) - { - // field value > 15 - upto++; - upto++; - end--; - } - else - { - // field value <= 15 - upto++; - end--; - } - } - - if (upto >= end) - return PARSE_ERROR; - - /* - DBG_PRINTF("sto_subarray called, looking for index %u\n", index_id); - for (int j = -5; j < 5; ++j) - printf(( j == 0 ? " >%02X< " : " %02X "), *(start + j)); - DBG_PRINTF("\n"); - */ - for (int i = 0; i < 1024 && upto < end; ++i) - { - int type = -1, field = -1, payload_start = -1, payload_length = -1; - int32_t length = get_stobject_length( - upto, end, type, field, payload_start, payload_length, 0); - if (length < 0) - return PARSE_ERROR; - - if (i == index_id) - { - DBG_PRINTF("sto_subarray returned for index %u\n", index_id); - for (int j = -5; j < 5; ++j) - DBG_PRINTF( - (j == 0 ? " [%02X] " : " %02X "), *(upto + j + length)); - DBG_PRINTF("\n"); - - return (((int64_t)(upto - start)) << 32U) /* start of the object */ - + (int64_t)(length); - } - upto += length; - } - - if (upto != end) - return PARSE_ERROR; - - return DOESNT_EXIST; + Bytes data{memory + read_ptr, memory + read_ptr + read_len}; + auto const result = api.sto_subarray(data, index_id); + if (!result) + return result.error(); + auto const& pair = result.value(); + return (uint64_t(pair.first) << 32U) + (uint32_t)pair.second; HOOK_TEARDOWN(); } @@ -4311,11 +2910,11 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - if (read_len != 20) - return INVALID_ARGUMENT; - - std::string raddr = - encodeBase58Token(TokenType::AccountID, memory + read_ptr, read_len); + auto const result = + api.util_raddr(Bytes{memory + read_ptr, memory + read_ptr + read_len}); + if (!result) + return result.error(); + auto const& raddr = result.value(); if (write_len < raddr.size()) return TOO_SMALL; @@ -4366,12 +2965,13 @@ DEFINE_HOOK_FUNCTION( std::string raddr{buffer}; - auto const result = decodeBase58Token(raddr, TokenType::AccountID); - if (result.empty()) - return INVALID_ARGUMENT; + auto const result = api.util_accid(raddr); + if (!result) + return result.error(); + auto const& accountID = result.value(); WRITE_WASM_MEMORY_AND_RETURN( - write_ptr, write_len, result.data(), 20, memory, memory_length); + write_ptr, write_len, accountID.data(), 20, memory, memory_length); HOOK_TEARDOWN(); } @@ -4484,116 +3084,25 @@ DEFINE_HOOK_FUNCTION( return MEM_OVERLAP; } - if (fread_len > 0 && view.rules().enabled(fixHookAPI20251128)) - { - // inject field should be valid sto object and it's field id should - // match the field_id - unsigned char* inject_start = (unsigned char*)(memory + fread_ptr); - unsigned char* inject_end = - (unsigned char*)(memory + fread_ptr + fread_len); - int type = -1, field = -1, payload_start = -1, payload_length = -1; - int32_t length = get_stobject_length( - inject_start, - inject_end, - type, - field, - payload_start, - payload_length, - 0); - if (length < 0) - return PARSE_ERROR; - if ((type << 16) + field != field_id) - { - return PARSE_ERROR; - } - } + Bytes source{memory + sread_ptr, memory + sread_ptr + sread_len}; + std::optional field; + if (fread_len > 0 && fread_ptr > 0) + field = Bytes{memory + fread_ptr, memory + fread_ptr + fread_len}; + auto const result = api.sto_emplace(source, field, field_id); + if (!result) + return result.error(); + auto const& bytes = result.value(); - // we must inject the field at the canonical location.... - // so find that location - unsigned char* start = (unsigned char*)(memory + sread_ptr); - unsigned char* upto = start; - unsigned char* end = start + sread_len; - unsigned char* inject_start = end; - unsigned char* inject_end = end; + if (bytes.size() > write_len) + return INTERNAL_ERROR; - DBG_PRINTF( - "sto_emplace called, looking for field %u type %u\n", - field_id & 0xFFFF, - (field_id >> 16)); - for (int j = -5; j < 5; ++j) - DBG_PRINTF((j == 0 ? " >%02X< " : " %02X "), *(start + j)); - DBG_PRINTF("\n"); - - for (int i = 0; i < 1024 && upto < end; ++i) - { - int type = -1, field = -1, payload_start = -1, payload_length = -1; - int32_t length = get_stobject_length( - upto, end, type, field, payload_start, payload_length, 0); - if (length < 0) - return PARSE_ERROR; - if ((type << 16) + field == field_id) - { - inject_start = upto; - inject_end = upto + length; - break; - } - else if ((type << 16) + field > field_id) - { - inject_start = upto; - inject_end = upto; - break; - } - upto += length; - } - - // if the scan loop ends past the end of the source object - // then the source object is invalid/corrupt, so we must - // return an error - if (upto > end) - return PARSE_ERROR; - - // upto is injection point - int64_t bytes_written = 0; - - // part 1 - if (inject_start - start > 0) - { - WRITE_WASM_MEMORY( - bytes_written, - write_ptr, - write_len, - start, - (inject_start - start), - memory, - memory_length); - } - - if (fread_len > 0) - { - // write the field (or don't if it's a delete operation) - WRITE_WASM_MEMORY( - bytes_written, - (write_ptr + bytes_written), - (write_len - bytes_written), - memory + fread_ptr, - fread_len, - memory, - memory_length); - } - - // part 2 - if (end - inject_end > 0) - { - WRITE_WASM_MEMORY( - bytes_written, - (write_ptr + bytes_written), - (write_len - bytes_written), - inject_end, - (end - inject_end), - memory, - memory_length); - } - return bytes_written; + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, + write_len, + bytes.data(), + bytes.size(), + memory, + memory_length); HOOK_TEARDOWN(); } @@ -4642,24 +3151,11 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - if (read_len < 2) - return TOO_SMALL; - - unsigned char* start = (unsigned char*)(memory + read_ptr); - unsigned char* upto = start; - unsigned char* end = start + read_len; - - for (int i = 0; i < 1024 && upto < end; ++i) - { - int type = -1, field = -1, payload_start = -1, payload_length = -1; - int32_t length = get_stobject_length( - upto, end, type, field, payload_start, payload_length, 0); - if (length < 0) - return 0; - upto += length; - } - - return upto == end ? 1 : 0; + Bytes data{read_ptr + memory, read_ptr + read_len + memory}; + auto const result = api.sto_validate(data); + if (!result) + return result.error(); + return result.value() ? 1 : 0; HOOK_TEARDOWN(); } @@ -4685,27 +3181,17 @@ DEFINE_HOOK_FUNCTION( NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length)) return OUT_OF_BOUNDS; - if (kread_len != 33) - return INVALID_KEY; - - if (dread_len == 0) - return TOO_SMALL; - - if (sread_len < 30) - return TOO_SMALL; - - ripple::Slice keyslice{ + ripple::Slice key{ reinterpret_cast(kread_ptr + memory), kread_len}; ripple::Slice data{ reinterpret_cast(dread_ptr + memory), dread_len}; ripple::Slice sig{ reinterpret_cast(sread_ptr + memory), sread_len}; - if (!publicKeyType(keyslice)) - return INVALID_KEY; - - ripple::PublicKey key{keyslice}; - return verify(key, data, sig, false) ? 1 : 0; + auto const result = api.util_verify(data, sig, key); + if (!result) + return result.error(); + return result.value() ? 1 : 0; HOOK_TEARDOWN(); } @@ -4716,7 +3202,7 @@ DEFINE_HOOK_FUNCTION(int64_t, fee_base) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - return view.fees().base.drops(); + return api.fee_base(); HOOK_TEARDOWN(); } @@ -4729,41 +3215,15 @@ DEFINE_HOOK_FUNCTION( uint32_t read_ptr, uint32_t read_len) { - HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, - // hookCtx on current stack - + HOOK_SETUP(); if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - - if (hookCtx.expected_etxn_count <= -1) - return PREREQUISITE_NOT_MET; - - try - { - ripple::Slice tx{ - reinterpret_cast(read_ptr + memory), read_len}; - - SerialIter sitTrans(tx); - - std::unique_ptr stpTrans; - stpTrans = std::make_unique(std::ref(sitTrans)); - - if (!view.rules().enabled(fixHookAPI20251128)) - return Transactor::calculateBaseFee( - *(applyCtx.app.openLedger().current()), *stpTrans) - .drops(); - - return invoke_calculateBaseFee( - *(applyCtx.app.openLedger().current()), *stpTrans) - .drops(); - } - catch (std::exception& e) - { - JLOG(j.trace()) << "HookInfo[" << HC_ACC() - << "]: etxn_fee_base exception: " << e.what(); - return INVALID_TXN; - } - + ripple::Slice tx{ + reinterpret_cast(read_ptr + memory), read_len}; + auto const fee_base = api.etxn_fee_base(tx); + if (!fee_base) + return fee_base.error(); + return fee_base.value(); HOOK_TEARDOWN(); } @@ -4787,68 +3247,10 @@ DEFINE_HOOK_FUNCTION( if (write_len < expected_size) return TOO_SMALL; - if (hookCtx.expected_etxn_count <= -1) - return PREREQUISITE_NOT_MET; - - uint32_t generation = (uint32_t)(etxn_generation( - hookCtx, frameCtx)); // always non-negative so cast is safe - - int64_t burden = etxn_burden(hookCtx, frameCtx); - if (burden < 1) - return FEE_TOO_LARGE; - - unsigned char* out = memory + write_ptr; - - *out++ = 0xEDU; // begin sfEmitDetails /* upto = - // 0 | size = 1 */ - *out++ = 0x20U; // sfEmitGeneration preamble /* upto = - // 1 | size = 6 */ - *out++ = 0x2EU; // preamble cont - *out++ = (generation >> 24U) & 0xFFU; - *out++ = (generation >> 16U) & 0xFFU; - *out++ = (generation >> 8U) & 0xFFU; - *out++ = (generation >> 0U) & 0xFFU; - *out++ = 0x3DU; // sfEmitBurden preamble /* upto - // = 7 | size = 9 */ - *out++ = (burden >> 56U) & 0xFFU; - *out++ = (burden >> 48U) & 0xFFU; - *out++ = (burden >> 40U) & 0xFFU; - *out++ = (burden >> 32U) & 0xFFU; - *out++ = (burden >> 24U) & 0xFFU; - *out++ = (burden >> 16U) & 0xFFU; - *out++ = (burden >> 8U) & 0xFFU; - *out++ = (burden >> 0U) & 0xFFU; - *out++ = 0x5BU; // sfEmitParentTxnID preamble /* upto - // = 16 | size = 33 */ - if (otxn_id(hookCtx, frameCtx, out - memory, 32, 1) != 32) - return INTERNAL_ERROR; - out += 32; - *out++ = 0x5CU; // sfEmitNonce /* upto - // = 49 | size = 33 */ - if (etxn_nonce(hookCtx, frameCtx, out - memory, 32) != 32) - return INTERNAL_ERROR; - out += 32; - *out++ = 0x5DU; // sfEmitHookHash preamble /* upto - // = 82 | size = 33 */ - for (int i = 0; i < 32; ++i) - *out++ = hookCtx.result.hookHash.data()[i]; - - if (hookCtx.result.hasCallback) - { - *out++ = 0x8AU; // sfEmitCallback preamble /* - // upto = 115 | size = 22 */ - *out++ = 0x14U; // preamble cont - if (hook_account(hookCtx, frameCtx, out - memory, 20) != 20) - return INTERNAL_ERROR; - out += 20; - } - *out++ = 0xE1U; // end object (sfEmitDetails) /* upto = - // 137 | size = 1 */ - /* upto = 138 | --------- */ - int64_t outlen = out - memory - write_ptr; - - DBG_PRINTF("emitdetails size = %d\n", outlen); - return outlen; + auto const result = api.etxn_details(memory + write_ptr); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -4971,71 +3373,14 @@ DEFINE_HOOK_FUNCTION(int64_t, float_set, int32_t exp, int64_t mantissa) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (mantissa == 0) - return 0; - - int64_t normalized = hook_float::normalize_xfl(mantissa, exp); - - // the above function will underflow into a canonical 0 - // but this api must report that underflow - if (normalized == 0 || normalized == XFL_OVERFLOW) - return INVALID_FLOAT; - - return normalized; + auto const result = api.float_set(exp, mantissa); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } -inline int64_t -mulratio_internal( - int64_t& man1, - int32_t& exp1, - bool round_up, - uint32_t numerator, - uint32_t denominator) -{ - try - { - ripple::IOUAmount amt{man1, exp1}; - ripple::IOUAmount out = ripple::mulRatio( - amt, numerator, denominator, round_up != 0); // already normalized - man1 = out.mantissa(); - exp1 = out.exponent(); - return 1; - } - catch (std::overflow_error& e) - { - return XFL_OVERFLOW; - } -} - -inline int64_t -float_multiply_internal_parts( - uint64_t man1, - int32_t exp1, - bool neg1, - uint64_t man2, - int32_t exp2, - bool neg2) -{ - using namespace boost::multiprecision; - cpp_int mult = cpp_int(man1) * cpp_int(man2); - mult /= power_of_ten[15]; - uint64_t man_out = static_cast(mult); - if (mult > man_out) - return XFL_OVERFLOW; - - int32_t exp_out = exp1 + exp2 + 15; - bool neg_out = (neg1 && !neg2) || (!neg1 && neg2); - int64_t ret = normalize_xfl(man_out, exp_out, neg_out); - - if (ret == EXPONENT_UNDERSIZED) - return 0; - if (ret == EXPONENT_OVERSIZED) - return XFL_OVERFLOW; - return ret; -} - DEFINE_HOOK_FUNCTION( int64_t, float_int, @@ -5047,33 +3392,11 @@ DEFINE_HOOK_FUNCTION( // hookCtx on current stack RETURN_IF_INVALID_FLOAT(float1); - if (float1 == 0) - return 0; - uint64_t man1 = get_mantissa(float1); - int32_t exp1 = get_exponent(float1); - bool neg1 = is_negative(float1); - if (decimal_places > 15) - return INVALID_ARGUMENT; - - if (neg1) - { - if (!absolute) - return CANT_RETURN_NEGATIVE; - } - - int32_t shift = -(exp1 + decimal_places); - - if (shift > 15) - return 0; - - if (shift < 0) - return TOO_BIG; - - if (shift > 0) - man1 /= power_of_ten[shift]; - - return man1; + auto const result = api.float_int(float1, decimal_places, absolute); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5086,17 +3409,10 @@ DEFINE_HOOK_FUNCTION(int64_t, float_multiply, int64_t float1, int64_t float2) RETURN_IF_INVALID_FLOAT(float1); RETURN_IF_INVALID_FLOAT(float2); - if (float1 == 0 || float2 == 0) - return 0; - - uint64_t man1 = get_mantissa(float1); - int32_t exp1 = get_exponent(float1); - bool neg1 = is_negative(float1); - uint64_t man2 = get_mantissa(float2); - int32_t exp2 = get_exponent(float2); - bool neg2 = is_negative(float2); - - return float_multiply_internal_parts(man1, exp1, neg1, man2, exp2, neg2); + auto const result = api.float_multiply(float1, float2); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5113,22 +3429,12 @@ DEFINE_HOOK_FUNCTION( // hookCtx on current stack RETURN_IF_INVALID_FLOAT(float1); - if (float1 == 0) - return 0; - if (denominator == 0) - return DIVISION_BY_ZERO; - int64_t man1 = (int64_t)get_mantissa(float1); - int32_t exp1 = get_exponent(float1); - - if (mulratio_internal(man1, exp1, round_up > 0, numerator, denominator) < 0) - return XFL_OVERFLOW; - - // defensive check - if (man1 < 0) - man1 *= -1LL; - - return make_float((uint64_t)man1, exp1, is_negative(float1)); + auto const result = + api.float_mulratio(float1, round_up, numerator, denominator); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5138,10 +3444,9 @@ DEFINE_HOOK_FUNCTION(int64_t, float_negate, int64_t float1) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (float1 == 0) - return 0; RETURN_IF_INVALID_FLOAT(float1); - return hook_float::invert_sign(float1); + + return api.float_negate(float1); HOOK_TEARDOWN(); } @@ -5159,46 +3464,10 @@ DEFINE_HOOK_FUNCTION( RETURN_IF_INVALID_FLOAT(float1); RETURN_IF_INVALID_FLOAT(float2); - bool equal_flag = mode & compare_mode::EQUAL; - bool less_flag = mode & compare_mode::LESS; - bool greater_flag = mode & compare_mode::GREATER; - bool not_equal = less_flag && greater_flag; - - if ((equal_flag && less_flag && greater_flag) || mode == 0) - return INVALID_ARGUMENT; - - if (mode & (~0b111UL)) - return INVALID_ARGUMENT; - - try - { - int64_t man1 = (int64_t)(get_mantissa(float1)) * - (is_negative(float1) ? -1LL : 1LL); - int32_t exp1 = get_exponent(float1); - ripple::IOUAmount amt1{man1, exp1}; - int64_t man2 = (int64_t)(get_mantissa(float2)) * - (is_negative(float2) ? -1LL : 1LL); - int32_t exp2 = get_exponent(float2); - ripple::IOUAmount amt2{man2, exp2}; - - if (not_equal && amt1 != amt2) - return 1; - - if (equal_flag && amt1 == amt2) - return 1; - - if (greater_flag && amt1 > amt2) - return 1; - - if (less_flag && amt1 < amt2) - return 1; - - return 0; - } - catch (std::overflow_error& e) - { - return XFL_OVERFLOW; - } + auto const result = api.float_compare(float1, float2, mode); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5211,36 +3480,10 @@ DEFINE_HOOK_FUNCTION(int64_t, float_sum, int64_t float1, int64_t float2) RETURN_IF_INVALID_FLOAT(float1); RETURN_IF_INVALID_FLOAT(float2); - if (float1 == 0) - return float2; - if (float2 == 0) - return float1; - - int64_t man1 = - (int64_t)(get_mantissa(float1)) * (is_negative(float1) ? -1LL : 1LL); - int32_t exp1 = get_exponent(float1); - int64_t man2 = - (int64_t)(get_mantissa(float2)) * (is_negative(float2) ? -1LL : 1LL); - int32_t exp2 = get_exponent(float2); - - try - { - ripple::IOUAmount amt1{man1, exp1}; - ripple::IOUAmount amt2{man2, exp2}; - amt1 += amt2; - int64_t result = make_float(amt1); - if (result == EXPONENT_UNDERSIZED) - { - // this is an underflow e.g. as a result of subtracting an xfl from - // itself and thus not an error, just return canonical 0 - return 0; - } - return result; - } - catch (std::overflow_error& e) - { - return XFL_OVERFLOW; - } + auto const result = api.float_sum(float1, float2); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5304,160 +3547,19 @@ DEFINE_HOOK_FUNCTION( RETURN_IF_INVALID_FLOAT(float1); - uint16_t field = field_code & 0xFFFFU; - uint16_t type = field_code >> 16U; + auto const result = + api.float_sto(currency, issuer, float1, field_code, write_len); + if (!result) + return result.error(); - bool is_xrp = field_code == 0; - bool is_short = - field_code == 0xFFFFFFFFU; // non-xrp value but do not output header or - // tail, just amount - - int bytes_needed = 8 + - (field == 0 && type == 0 - ? 0 - : (field == 0xFFFFU && type == 0xFFFFU - ? 0 - : (field < 16 && type < 16 - ? 1 - : (field >= 16 && type < 16 - ? 2 - : (field < 16 && type >= 16 ? 2 : 3))))); - - int64_t bytes_written = 0; - - if (issuer && !currency) - return INVALID_ARGUMENT; - - if (!issuer && currency) - return INVALID_ARGUMENT; - - if (issuer) - { - if (is_xrp) - return INVALID_ARGUMENT; - if (is_short) - return INVALID_ARGUMENT; - - bytes_needed += 40; - } - else if (!is_xrp && !is_short) - return INVALID_ARGUMENT; - - if (bytes_needed > write_len) - return TOO_SMALL; - - if (is_xrp || is_short) - { - // do nothing - } - else if (field < 16 && type < 16) - { - *(memory + write_ptr) = (((uint8_t)type) << 4U) + ((uint8_t)field); - bytes_written++; - } - else if (field >= 16 && type < 16) - { - *(memory + write_ptr) = (((uint8_t)type) << 4U); - *(memory + write_ptr + 1) = ((uint8_t)field); - bytes_written += 2; - } - else if (field < 16 && type >= 16) - { - *(memory + write_ptr) = (((uint8_t)field) << 4U); - *(memory + write_ptr + 1) = ((uint8_t)type); - bytes_written += 2; - } - else - { - *(memory + write_ptr) = 0; - *(memory + write_ptr + 1) = ((uint8_t)type); - *(memory + write_ptr + 2) = ((uint8_t)field); - bytes_written += 3; - } - - uint64_t man = get_mantissa(float1); - int32_t exp = get_exponent(float1); - bool neg = is_negative(float1); - uint8_t out[8]; - if (is_xrp) - { - int32_t shift = -(exp); - - if (shift > 15) - return 0; - - if (shift < 0) - return XFL_OVERFLOW; - - if (shift > 0) - man /= power_of_ten[shift]; - - out[0] = (neg ? 0b00000000U : 0b01000000U); - out[0] += (uint8_t)((man >> 56U) & 0b111111U); - out[1] = (uint8_t)((man >> 48U) & 0xFF); - out[2] = (uint8_t)((man >> 40U) & 0xFF); - out[3] = (uint8_t)((man >> 32U) & 0xFF); - out[4] = (uint8_t)((man >> 24U) & 0xFF); - out[5] = (uint8_t)((man >> 16U) & 0xFF); - out[6] = (uint8_t)((man >> 8U) & 0xFF); - out[7] = (uint8_t)((man >> 0U) & 0xFF); - } - else if (man == 0) - { - out[0] = 0b10000000U; - for (int i = 1; i < 8; ++i) - out[i] = 0; - } - else - { - exp += 97; - - /// encode the rippled floating point sto format - - out[0] = (neg ? 0b10000000U : 0b11000000U); - out[0] += (uint8_t)(exp >> 2U); - out[1] = ((uint8_t)(exp & 0b11U)) << 6U; - out[1] += (((uint8_t)(man >> 48U)) & 0b111111U); - out[2] = (uint8_t)((man >> 40U) & 0xFFU); - out[3] = (uint8_t)((man >> 32U) & 0xFFU); - out[4] = (uint8_t)((man >> 24U) & 0xFFU); - out[5] = (uint8_t)((man >> 16U) & 0xFFU); - out[6] = (uint8_t)((man >> 8U) & 0xFFU); - out[7] = (uint8_t)((man >> 0U) & 0xFFU); - } - - WRITE_WASM_MEMORY( - bytes_written, - write_ptr + bytes_written, - write_len - bytes_written, - out, - 8, + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, + write_len, + (*result).data(), + (*result).size(), memory, memory_length); - if (!is_xrp && !is_short) - { - WRITE_WASM_MEMORY( - bytes_written, - write_ptr + bytes_written, - write_len - bytes_written, - (*currency).data(), - 20, - memory, - memory_length); - - WRITE_WASM_MEMORY( - bytes_written, - write_ptr + bytes_written, - write_len - bytes_written, - (*issuer).data(), - 20, - memory, - memory_length); - } - - return bytes_written; - HOOK_TEARDOWN(); } @@ -5476,167 +3578,35 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) return OUT_OF_BOUNDS; - uint8_t* upto = memory + read_ptr; + Bytes data{read_ptr + memory, read_ptr + read_len + memory}; - if (read_len > 8) - { - uint8_t hi = memory[read_ptr] >> 4U; - uint8_t lo = memory[read_ptr] & 0xFU; - - if (hi == 0 && lo == 0) - { - // typecode >= 16 && fieldcode >= 16 - if (read_len < 11) - return NOT_AN_OBJECT; - upto += 3; - read_len -= 3; - } - else if (hi == 0 || lo == 0) - { - // typecode >= 16 && fieldcode < 16 - if (read_len < 10) - return NOT_AN_OBJECT; - upto += 2; - read_len -= 2; - } - else - { - // typecode < 16 && fieldcode < 16 - upto++; - read_len--; - } - } - - if (read_len < 8) - return NOT_AN_OBJECT; - - bool is_xrp = (((*upto) & 0b10000000U) == 0); - bool is_negative = (((*upto) & 0b01000000U) == 0); - - int32_t exponent = 0; - - if (is_xrp) - { - // exponent remains 0 - upto++; - } - else - { - exponent = (((*upto++) & 0b00111111U)) << 2U; - exponent += ((*upto) >> 6U); - exponent -= 97; - } - - uint64_t mantissa = (((uint64_t)(*upto++)) & 0b00111111U) << 48U; - mantissa += ((uint64_t)*upto++) << 40U; - mantissa += ((uint64_t)*upto++) << 32U; - mantissa += ((uint64_t)*upto++) << 24U; - mantissa += ((uint64_t)*upto++) << 16U; - mantissa += ((uint64_t)*upto++) << 8U; - mantissa += ((uint64_t)*upto++); - - if (mantissa == 0) - return 0; - - return hook_float::normalize_xfl(mantissa, exponent, is_negative); + auto const result = api.float_sto_set(data); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } -const int64_t float_one_internal = make_float(1000000000000000ull, -15, false); - -inline int64_t -float_divide_internal(int64_t float1, int64_t float2, bool hasFix) -{ - RETURN_IF_INVALID_FLOAT(float1); - RETURN_IF_INVALID_FLOAT(float2); - if (float2 == 0) - return DIVISION_BY_ZERO; - if (float1 == 0) - return 0; - - // special case: division by 1 - // RH TODO: add more special cases (division by power of 10) - if (float2 == float_one_internal) - return float1; - - uint64_t man1 = get_mantissa(float1); - int32_t exp1 = get_exponent(float1); - bool neg1 = is_negative(float1); - uint64_t man2 = get_mantissa(float2); - int32_t exp2 = get_exponent(float2); - bool neg2 = is_negative(float2); - - int64_t tmp1 = normalize_xfl(man1, exp1); - int64_t tmp2 = normalize_xfl(man2, exp2); - - if (tmp1 < 0 || tmp2 < 0) - return INVALID_FLOAT; - - if (tmp1 == 0) - return 0; - - while (man2 > man1) - { - man2 /= 10; - exp2++; - } - - if (man2 == 0) - return DIVISION_BY_ZERO; - - while (man2 < man1) - { - if (man2 * 10 > man1) - break; - man2 *= 10; - exp2--; - } - - uint64_t man3 = 0; - int32_t exp3 = exp1 - exp2; - - while (man2 > 0) - { - int i = 0; - if (hasFix) - { - for (; man1 >= man2; man1 -= man2, ++i) - ; - } - else - { - for (; man1 > man2; man1 -= man2, ++i) - ; - } - - man3 *= 10; - man3 += i; - man2 /= 10; - if (man2 == 0) - break; - exp3--; - } - - bool neg3 = !((neg1 && neg2) || (!neg1 && !neg2)); - - return normalize_xfl(man3, exp3, neg3); -} - DEFINE_HOOK_FUNCTION(int64_t, float_divide, int64_t float1, int64_t float2) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - bool const hasFix = view.rules().enabled(fixFloatDivide); - return float_divide_internal(float1, float2, hasFix); + RETURN_IF_INVALID_FLOAT(float1); + RETURN_IF_INVALID_FLOAT(float2); + + auto const result = api.float_divide(float1, float2); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } DEFINE_HOOK_FUNCTION(int64_t, float_one) { - return float_one_internal; + return hookCtx.api().float_one(); } DEFINE_HOOK_FUNCTION(int64_t, float_invert, int64_t float1) @@ -5644,13 +3614,12 @@ DEFINE_HOOK_FUNCTION(int64_t, float_invert, int64_t float1) HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, // hookCtx on current stack - if (float1 == 0) - return DIVISION_BY_ZERO; - if (float1 == float_one_internal) - return float_one_internal; + RETURN_IF_INVALID_FLOAT(float1); - bool const fixV3 = view.rules().enabled(fixFloatDivide); - return float_divide_internal(float_one_internal, float1, fixV3); + auto const result = api.float_invert(float1); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5661,9 +3630,11 @@ DEFINE_HOOK_FUNCTION(int64_t, float_mantissa, int64_t float1) // hookCtx on current stack RETURN_IF_INVALID_FLOAT(float1); - if (float1 == 0) - return 0; - return get_mantissa(float1); + + auto const result = api.float_mantissa(float1); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5674,62 +3645,12 @@ DEFINE_HOOK_FUNCTION(int64_t, float_sign, int64_t float1) // hookCtx on current stack RETURN_IF_INVALID_FLOAT(float1); - if (float1 == 0) - return 0; - return is_negative(float1); + + return api.float_sign(float1); HOOK_TEARDOWN(); } -inline int64_t -double_to_xfl(double x) -{ - if ((x) == 0) - return 0; - bool neg = x < 0; - double absresult = neg ? -x : x; - - // first compute the base 10 order of the float - int32_t exp_out = (int32_t)log10(absresult); - - // next adjust it into the valid mantissa range (this means dividing by its - // order and multiplying by 10**15) - absresult *= pow(10, -exp_out + 15); - - // after adjustment the value may still fall below the minMantissa - int64_t result = (int64_t)absresult; - if (result < minMantissa) - { - if (result == minMantissa - 1LL) - result += 1LL; - else - { - result *= 10LL; - exp_out--; - } - } - - // likewise the value can fall above the maxMantissa - if (result > maxMantissa) - { - if (result == maxMantissa + 1LL) - result -= 1LL; - else - { - result /= 10LL; - exp_out++; - } - } - - exp_out -= 15; - int64_t ret = make_float(result, exp_out, neg); - - if (ret == EXPONENT_UNDERSIZED) - return 0; - - return ret; -} - DEFINE_HOOK_FUNCTION(int64_t, float_log, int64_t float1) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, @@ -5737,18 +3658,10 @@ DEFINE_HOOK_FUNCTION(int64_t, float_log, int64_t float1) RETURN_IF_INVALID_FLOAT(float1); - if (float1 == 0) - return INVALID_ARGUMENT; - - uint64_t man1 = get_mantissa(float1); - int32_t exp1 = get_exponent(float1); - if (is_negative(float1)) - return COMPLEX_NOT_SUPPORTED; - - double inp = (double)(man1); - double result = log10(inp) + exp1; - - return double_to_xfl(result); + auto const result = api.float_log(float1); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5759,21 +3672,11 @@ DEFINE_HOOK_FUNCTION(int64_t, float_root, int64_t float1, uint32_t n) // hookCtx on current stack RETURN_IF_INVALID_FLOAT(float1); - if (float1 == 0) - return 0; - if (n < 2) - return INVALID_ARGUMENT; - - uint64_t man1 = get_mantissa(float1); - int32_t exp1 = get_exponent(float1); - if (is_negative(float1)) - return COMPLEX_NOT_SUPPORTED; - - double inp = (double)(man1)*pow(10, exp1); - double result = pow(inp, ((double)1.0f) / ((double)(n))); - - return double_to_xfl(result); + auto const result = api.float_root(float1, n); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5795,46 +3698,18 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) return OUT_OF_BOUNDS; - if (read_len < 1) + Bytes paramName{read_ptr + memory, read_ptr + read_len + memory}; + + auto const result = api.otxn_param(paramName); + if (!result) + return result.error(); + auto const& val = result.value(); + + if (val.size() > write_len) return TOO_SMALL; - if (read_len > 32) - return TOO_BIG; - - if (!applyCtx.tx.isFieldPresent(sfHookParameters)) - return DOESNT_EXIST; - - std::vector paramName{ - read_ptr + memory, read_ptr + read_len + memory}; - - auto const& params = applyCtx.tx.getFieldArray(sfHookParameters); - - for (auto const& param : params) - { - if (!param.isFieldPresent(sfHookParameterName) || - param.getFieldVL(sfHookParameterName) != paramName) - continue; - - if (!param.isFieldPresent(sfHookParameterValue)) - return DOESNT_EXIST; - - auto const& val = param.getFieldVL(sfHookParameterValue); - if (val.empty()) - return DOESNT_EXIST; - - if (val.size() > write_len) - return TOO_SMALL; - - WRITE_WASM_MEMORY_AND_RETURN( - write_ptr, - write_len, - val.data(), - val.size(), - memory, - memory_length); - } - - return DOESNT_EXIST; + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, val.data(), val.size(), memory, memory_length); HOOK_TEARDOWN(); } @@ -5856,54 +3731,17 @@ DEFINE_HOOK_FUNCTION( if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) return OUT_OF_BOUNDS; - if (read_len < 1) - return TOO_SMALL; + Bytes paramName{read_ptr + memory, read_ptr + read_len + memory}; - if (read_len > 32) - return TOO_BIG; + auto const result = api.hook_param(paramName); - std::vector paramName{ - read_ptr + memory, read_ptr + read_len + memory}; + if (!result) + return result.error(); - // first check for overrides set by prior hooks in the chain - auto const& overrides = hookCtx.result.hookParamOverrides; - if (overrides.find(hookCtx.result.hookHash) != overrides.end()) - { - auto const& params = overrides.at(hookCtx.result.hookHash); - if (params.find(paramName) != params.end()) - { - auto const& param = params.at(paramName); - if (param.size() == 0) - return DOESNT_EXIST; // allow overrides to "delete" parameters + auto const& val = result.value(); - WRITE_WASM_MEMORY_AND_RETURN( - write_ptr, - write_len, - param.data(), - param.size(), - memory, - memory_length); - } - } - - // next check if there's a param set on this hook - auto const& params = hookCtx.result.hookParams; - if (params.find(paramName) != params.end()) - { - auto const& param = params.at(paramName); - if (param.size() == 0) - return DOESNT_EXIST; - - WRITE_WASM_MEMORY_AND_RETURN( - write_ptr, - write_len, - param.data(), - param.size(), - memory, - memory_length); - } - - return DOESNT_EXIST; + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, val.data(), val.size(), memory, memory_length); HOOK_TEARDOWN(); } @@ -5926,40 +3764,30 @@ DEFINE_HOOK_FUNCTION( NOT_IN_BOUNDS(hread_ptr, hread_len, memory_length)) return OUT_OF_BOUNDS; - if (kread_len < 1) - return TOO_SMALL; + { + // those checks are also done in the HookAPI + // but we need to check them here too for backwards compatibility + if (kread_len < 1) + return TOO_SMALL; - if (kread_len > hook::maxHookParameterKeySize()) - return TOO_BIG; + if (kread_len > hook::maxHookParameterKeySize()) + return TOO_BIG; - if (hread_len != 32) - return INVALID_ARGUMENT; + if (hread_len != 32) + return INVALID_ARGUMENT; - if (read_len > hook::maxHookParameterValueSize()) - return TOO_BIG; - - std::vector paramName{ - kread_ptr + memory, kread_ptr + kread_len + memory}; - std::vector paramValue{ - read_ptr + memory, read_ptr + read_len + memory}; + if (read_len > hook::maxHookParameterValueSize()) + return TOO_BIG; + } + Bytes paramName{kread_ptr + memory, kread_ptr + kread_len + memory}; + Bytes paramValue{read_ptr + memory, read_ptr + read_len + memory}; ripple::uint256 hash = ripple::uint256::fromVoid(memory + hread_ptr); - if (hookCtx.result.overrideCount >= hook_api::max_params) - return TOO_MANY_PARAMS; - - hookCtx.result.overrideCount++; - - auto& overrides = hookCtx.result.hookParamOverrides; - if (overrides.find(hash) == overrides.end()) - { - overrides[hash] = std::map, std::vector>{ - {std::move(paramName), std::move(paramValue)}}; - } - else - overrides[hash][std::move(paramName)] = std::move(paramValue); - - return read_len; + auto const result = api.hook_param_set(hash, paramName, paramValue); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } @@ -5980,75 +3808,31 @@ DEFINE_HOOK_FUNCTION( if (read_len != 32) return INVALID_ARGUMENT; - if (flags != 0 && flags != 1) - return INVALID_ARGUMENT; - - auto& skips = hookCtx.result.hookSkips; ripple::uint256 hash = ripple::uint256::fromVoid(memory + read_ptr); - if (flags == 1) - { - // delete flag - if (skips.find(hash) == skips.end()) - return DOESNT_EXIST; - skips.erase(hash); - return 1; - } - - // first check if it's already in the skips set - if (skips.find(hash) != skips.end()) - return 1; - - // next check if it's even in this chain - std::shared_ptr hookSLE = - applyCtx.view().peek(hookCtx.result.hookKeylet); - - if (!hookSLE || !hookSLE->isFieldPresent(sfHooks)) - return INTERNAL_ERROR; - - ripple::STArray const& hooks = hookSLE->getFieldArray(sfHooks); - bool found = false; - for (auto const& hookObj : hooks) - { - if (hookObj.isFieldPresent(sfHookHash)) - { - if (hookObj.getFieldH256(sfHookHash) == hash) - { - found = true; - break; - } - } - } - - if (!found) - return DOESNT_EXIST; - - // finally add it to the skips list - hookCtx.result.hookSkips.emplace(hash); - return 1; + auto const result = api.hook_skip(hash, flags); + if (!result) + return result.error(); + return result.value(); HOOK_TEARDOWN(); } DEFINE_HOOK_FUNCTION(int64_t, hook_pos) { - return hookCtx.result.hookChainPosition; + return hookCtx.api().hook_pos(); } DEFINE_HOOK_FUNCTION(int64_t, hook_again) { HOOK_SETUP(); - if (hookCtx.result.executeAgainAsWeak) - return ALREADY_SET; + auto const result = api.hook_again(); - if (hookCtx.result.isStrong) - { - hookCtx.result.executeAgainAsWeak = true; - return 1; - } + if (!result) + return result.error(); - return PREREQUISITE_NOT_MET; + return result.value(); HOOK_TEARDOWN(); } @@ -6057,30 +3841,11 @@ DEFINE_HOOK_FUNCTION(int64_t, meta_slot, uint32_t slot_into) { HOOK_SETUP(); - if (!hookCtx.result.provisionalMeta) - return PREREQUISITE_NOT_MET; + auto const result = api.meta_slot(slot_into); + if (!result) + return result.error(); - if (slot_into > hook_api::max_slots) - return INVALID_ARGUMENT; - - // check if we can emplace the object to a slot - if (slot_into == 0 && no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - if (slot_into == 0) - { - if (auto found = get_free_slot(hookCtx); found) - slot_into = *found; - else - return NO_FREE_SLOTS; - } - - hookCtx.slot[slot_into] = - hook::SlotEntry{.storage = hookCtx.result.provisionalMeta, .entry = 0}; - - hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); - - return slot_into; + return result.value(); HOOK_TEARDOWN(); } @@ -6093,66 +3858,11 @@ DEFINE_HOOK_FUNCTION( { HOOK_SETUP(); - if (applyCtx.tx.getFieldU16(sfTransactionType) != ttIMPORT) - return PREREQUISITE_NOT_MET; + auto const result = api.xpop_slot(slot_into_tx, slot_into_meta); + if (!result) + return result.error(); - if (slot_into_tx > hook_api::max_slots || - slot_into_meta > hook_api::max_slots) - return INVALID_ARGUMENT; - - size_t free_count = hook_api::max_slots - hookCtx.slot.size(); - - size_t needed_count = slot_into_tx == 0 && slot_into_meta == 0 - ? 2 - : slot_into_tx != 0 && slot_into_meta != 0 ? 0 : 1; - - if (free_count < needed_count) - return NO_FREE_SLOTS; - - // if they supply the same slot number for both (other than 0) - // they will produce a collision - if (needed_count == 0 && slot_into_tx == slot_into_meta) - return INVALID_ARGUMENT; - - if (slot_into_tx == 0) - { - if (no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - if (auto found = get_free_slot(hookCtx); found) - slot_into_tx = *found; - else - return NO_FREE_SLOTS; - } - - if (slot_into_meta == 0) - { - if (no_free_slots(hookCtx)) - return NO_FREE_SLOTS; - - if (auto found = get_free_slot(hookCtx); found) - slot_into_meta = *found; - else - return NO_FREE_SLOTS; - } - - auto [tx, meta] = Import::getInnerTxn(applyCtx.tx, j); - - if (!tx || !meta) - return INVALID_TXN; - - hookCtx.slot[slot_into_tx] = - hook::SlotEntry{.storage = std::move(tx), .entry = 0}; - - hookCtx.slot[slot_into_tx].entry = &(*hookCtx.slot[slot_into_tx].storage); - - hookCtx.slot[slot_into_meta] = - hook::SlotEntry{.storage = std::move(meta), .entry = 0}; - - hookCtx.slot[slot_into_meta].entry = - &(*hookCtx.slot[slot_into_meta].storage); - - return (slot_into_tx << 16U) + slot_into_meta; + return std::get<0>(result.value()) << 16U | std::get<1>(result.value()); HOOK_TEARDOWN(); } diff --git a/src/ripple/basics/Expected.h b/src/ripple/basics/Expected.h index bb699579b..b919e9fd6 100644 --- a/src/ripple/basics/Expected.h +++ b/src/ripple/basics/Expected.h @@ -137,14 +137,14 @@ class [[nodiscard]] Expected public: template requires std::convertible_to constexpr Expected(U && r) - : Base(T(std::forward(r))) + : Base(boost::outcome_v2::success(T(std::forward(r)))) { } template requires std::convertible_to && (!std::is_reference_v)constexpr Expected(Unexpected e) - : Base(E(std::move(e.value()))) + : Base(boost::outcome_v2::failure(E(std::move(e.value())))) { } @@ -220,7 +220,7 @@ public: template requires std::convertible_to && (!std::is_reference_v)constexpr Expected(Unexpected e) - : Base(E(std::move(e.value()))) + : Base(boost::outcome_v2::failure(E(std::move(e.value())))) { } diff --git a/src/test/app/HookAPI_test.cpp b/src/test/app/HookAPI_test.cpp new file mode 100644 index 000000000..1e3dc9dc7 --- /dev/null +++ b/src/test/app/HookAPI_test.cpp @@ -0,0 +1,4543 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 XRPL-Labs + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +namespace test { + +class HookAPI_test : public beast::unit_test::suite +{ +private: + ApplyContext + createApplyContext(jtx::Env& env, OpenView& ov, STTx const& tx) + { + ApplyContext applyCtx{ + env.app(), + ov, + tx, + tesSUCCESS, + env.current()->fees().base, + tapNONE, + env.journal}; + return applyCtx; + } + +public: + void + test_accept(FeatureBitset features) + { + testcase("Test accept() hookapi"); + + // TODO + BEAST_EXPECT(true); + } + + void + test_rollback(FeatureBitset features) + { + testcase("Test rollback() hookapi"); + + // TODO + BEAST_EXPECT(true); + } + + void + testGuards(FeatureBitset features) + { + testcase("Test guards"); + + // TODO + BEAST_EXPECT(true); + } + + void + test_emit(FeatureBitset features) + { + testcase("Test emit"); + using namespace jtx; + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + + using namespace hook_api; + Env env{*this, features}; + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + STTx const emitInvokeTx = STTx(ttINVOKE, [&](STObject& obj) { + obj[sfAccount] = alice.id(); + obj[sfSequence] = 0; + obj[sfSigningPubKey] = PublicKey(); + obj[sfFirstLedgerSequence] = env.closed()->seq() + 1; + obj[sfLastLedgerSequence] = env.closed()->seq() + 5; + obj[sfFee] = env.closed()->fees().base; + + auto& emitDetails = obj.peekFieldObject(sfEmitDetails); + emitDetails[sfEmitGeneration] = 1; + emitDetails[sfEmitBurden] = 1; + emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID(); + emitDetails[sfEmitNonce] = uint256(); + emitDetails[sfEmitHookHash] = uint256(); + }); + + STTx const emitSetHookTx = STTx(ttHOOK_SET, [&](STObject& obj) { + obj[sfAccount] = alice.id(); + obj[sfSequence] = 0; + obj[sfSigningPubKey] = PublicKey(); + obj[sfFirstLedgerSequence] = env.closed()->seq() + 1; + obj[sfLastLedgerSequence] = env.closed()->seq() + 5; + obj[sfFee] = env.closed()->fees().base; + STObject hookobj(sfHook); + auto& hooks = obj.peekFieldArray(sfHooks); + hooks.emplace_back(std::move(hookobj)); + + auto& emitDetails = obj.peekFieldObject(sfEmitDetails); + emitDetails[sfEmitGeneration] = 1; + emitDetails[sfEmitBurden] = 1; + emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID(); + emitDetails[sfEmitNonce] = uint256(); + emitDetails[sfEmitHookHash] = uint256(); + }); + + { + // PREREQUISITE_NOT_MET + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {.expected_etxn_count = -1}); + auto& api = hookCtx.api(); + + auto const result = api.emit(emitInvokeTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + { + // TOO_MANY_EMITTED_TXN + std::string reason; + auto tx = std::make_shared( + std::make_shared(invokeTx), + reason, + env.app()); + std::queue> emittedTxn; + emittedTxn.push(tx); + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .result = {.emittedTxn = emittedTxn}, + }); + auto& api = hookCtx.api(); + + auto const result = api.emit(emitInvokeTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == TOO_MANY_EMITTED_TXN); + } + // EMISSION_FAILURE + { + // Invalid txn + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + }); + auto& api = hookCtx.api(); + auto tx = emitInvokeTx; + Serializer s = tx.getSerializer(); + s.add8(0); // invalid value + auto const result = api.emit(s.slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Pseudo txn + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + }); + auto& api = hookCtx.api(); + auto tx = emitInvokeTx; + tx.setFieldU16(sfTransactionType, ttFEE); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // HookCanEmit (non-SetHook) + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + {.expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = { + .hookCanEmit = UINT256_BIT[ttINVOKE], + }}); + auto& api = hookCtx.api(); + auto tx = emitInvokeTx; + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // HookCanEmit (SetHook) Error + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + auto& api = hookCtx.api(); + auto const result = api.emit(emitSetHookTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // HookCanEmit (SetHook) Success + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = UINT256_BIT[ttHOOK_SET]}, + }); + auto& api = hookCtx.api(); + auto tx = emitSetHookTx; + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.has_value()); + } + + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = 1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + auto& api = hookCtx.api(); + { + // Invalid sfAccount + auto tx = emitInvokeTx; + { + // Missing sfAccount + tx.makeFieldAbsent(sfAccount); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfAccount (!= HookAccount) + tx.setAccountID(sfAccount, bob.id()); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfSequence + auto tx = emitInvokeTx; + { + // Missing sfSequence + tx.makeFieldAbsent(sfSequence); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfSequence (non-zero) + tx.setFieldU32(sfSequence, 1); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfSigningPubKey + auto tx = emitInvokeTx; + { + // Missing sfSigningPubKey + tx.makeFieldAbsent(sfSigningPubKey); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfSigningPubKey (wrong size) + for (int i = 1; i < 33; ++i) + { + tx.setFieldVL(sfSigningPubKey, std::vector(i, 0)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfSigningPubKey (non-zero) + for (int i = 0; i < 33; ++i) + { + auto vec = std::vector(33, 0); + vec[i] = 1; + tx.setFieldVL(sfSigningPubKey, vec); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + } + { + // Invalid sfSigners + auto tx = emitInvokeTx; + tx.setFieldArray(sfSigners, STArray(sfSigners, 1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfTicketSequence + auto tx = emitInvokeTx; + tx.setFieldU32(sfTicketSequence, 1); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfAccountTxnID + auto tx = emitInvokeTx; + tx.setFieldH256(sfAccountTxnID, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + ; // Invalid sfEmitDetails + { + // Missing sfEmitDetails + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfEmitDetails); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + std::vector> + detail_fields = { + sfEmitGeneration, + sfEmitBurden, + sfEmitParentTxnID, + sfEmitNonce, + sfEmitHookHash, + }; + // Missing fields in sfEmitDetails + for (auto const& rf : detail_fields) + { + SField const& field = rf.get(); + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + details.makeFieldAbsent(field); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + // TODO: test callback + { + ; // Invalid sfEmitGeneration + { + // Over Max sfEmitGeneration + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + details.setFieldU32(sfEmitGeneration, 11); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfEmitGeneration + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + details.setFieldU32( + sfEmitGeneration, hookCtx.generation + 2); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfEmitBurden + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT(hookCtx.burden == 0); + details.setFieldU64(sfEmitBurden, 2); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfEmitParentTxnID + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT(applyCtx.tx.getTransactionID() != uint256(1)); + details.setFieldH256(sfEmitParentTxnID, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfEmitNonce + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT( + hookCtx.nonce_used.find(uint256(1)) == + hookCtx.nonce_used.end()); + details.setFieldH256(sfEmitNonce, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + // TODO: test Callback + { + // Invalid sfEmitHookHash + auto tx = emitInvokeTx; + auto& details = tx.peekFieldObject(sfEmitDetails); + BEAST_EXPECT(hookCtx.result.hookHash != uint256(1)); + details.setFieldH256(sfEmitHookHash, uint256(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Invalid sfTxnSignature + auto tx = emitInvokeTx; + tx.setFieldVL(sfTxnSignature, std::vector(1, 0)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + ; // Invalid sfLastLedgerSequence + { + // Missing sfLastLedgerSequence + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfLastLedgerSequence); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfLastLedgerSequence + // (smaller than next ledger seq) + auto tx = emitInvokeTx; + auto const currentSeq = applyCtx.view().info().seq; + tx.setFieldU32(sfLastLedgerSequence, currentSeq); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfLastLedgerSequence + // (greater than current ledger seq + 5) + auto tx = emitInvokeTx; + auto const currentSeq = applyCtx.view().info().seq; + tx.setFieldU32(sfLastLedgerSequence, currentSeq + 6); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + ; // Invalid sfFirstLedgerSequence + { + // missing sfFirstLedgerSequence + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfFirstLedgerSequence); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfFirstLedgerSequence + auto tx = emitInvokeTx; + auto const lastLedgerSeq = tx.getFieldU32(sfLastLedgerSequence); + tx.setFieldU32(sfFirstLedgerSequence, lastLedgerSeq + 1); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + ; // Invalid sfFee + { + // Missing sfFee + auto tx = emitInvokeTx; + tx.makeFieldAbsent(sfFee); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Invalid sfFee + auto tx = emitInvokeTx; + tx.setFieldAmount(sfFee, drops(1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + } + { + // Preflight failure + auto tx = emitInvokeTx; + tx.setFieldVL(sfBlob, std::vector(128 * 1024 + 1, 1)); + auto const result = api.emit(tx.getSerializer().slice()); + BEAST_EXPECT(result.error() == EMISSION_FAILURE); + } + { + // Success + auto tx = emitInvokeTx; + Serializer s; + tx.add(s); + auto const result = api.emit(s.slice()); + BEAST_EXPECT(result.has_value()); + } + } + + void + test_etxn_details(FeatureBitset features) + { + testcase("Test etxn_details"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // PREREQUISITE_NOT_MET + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // FEE_TOO_LARGE (via etxn_burden overflow) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .burden = std::numeric_limits::max(), + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == FEE_TOO_LARGE); + } + + { + // SUCCESS path length check and nonce increment (no-callback) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .result = {.hookHash = uint256{3}}, + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 116); + BEAST_EXPECT(hookCtx.emit_nonce_counter == 1); + } + + { + // SUCCESS path length check and nonce increment (callback) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .result = {.hookHash = uint256{3}, .hasCallback = true}, + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + std::array buffer{}; + auto const result = api.etxn_details(buffer.data()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 138); + BEAST_EXPECT(hookCtx.emit_nonce_counter == 1); + } + } + + void + test_etxn_fee_base(FeatureBitset features) + { + testcase("Test etxn_fee_base"); + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + hook::HookContext hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = -1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + auto& api = hookCtx.api(); + + // PREREQUISITE_NOT_MET + { + auto const result = + api.etxn_fee_base(invokeTx.getSerializer().slice()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + hookCtx.expected_etxn_count = 1; + + // INVALID_TXN + { + auto tx = invokeTx; + Serializer s = tx.getSerializer(); + s.add8(0); // invalid value + auto const result = api.etxn_fee_base(s.slice()); + BEAST_EXPECT(result.error() == INVALID_TXN); + } + { + // SUCCESS + auto const result = + api.etxn_fee_base(invokeTx.getSerializer().slice()); + BEAST_EXPECT(result.has_value()); + auto const baseFee = env.closed()->fees().base; + BEAST_EXPECT(result.value() == baseFee); + } + { + // Fee value + auto tx = invokeTx; + // add 100 bytes of blob + tx.setFieldVL(sfBlob, std::vector(100, 1)); + // add 100 bytes of memo + tx.setFieldArray(sfMemos, STArray(sfMemos, 1)); + auto& memos = tx.peekFieldArray(sfMemos); + STObject memo = STObject(sfMemo); + memo.setFieldVL(sfMemoData, std::vector(100, 1)); + memos.emplace_back(memo); + auto const result = api.etxn_fee_base(tx.getSerializer().slice()); + BEAST_EXPECT(result.has_value()); + auto const baseFee = env.closed()->fees().base; + auto const blobSize = 100; + auto const memoSize = 100; + if (env.closed()->rules().enabled(fixHookAPI20251128)) + BEAST_EXPECT(result.value() == baseFee + blobSize + memoSize); + else + BEAST_EXPECT(result.value() == baseFee + memoSize); + } + } + + void + test_etxn_burden(FeatureBitset features) + { + testcase("Test etxn_burden"); + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // PREREQUISITE_NOT_MET + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.etxn_burden(); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // FEE_TOO_LARGE (overflow) + StubHookContext stubCtx{ + .expected_etxn_count = 2, + .burden = std::numeric_limits::max(), + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.etxn_burden(); + BEAST_EXPECT(result.error() == FEE_TOO_LARGE); + } + + { + // SUCCESS + StubHookContext stubCtx{.expected_etxn_count = 3, .burden = 5}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.etxn_burden(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 15); + } + } + + void + test_etxn_generation(FeatureBitset features) + { + testcase("Test etxn_generation"); + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx baseTx = STTx(ttINVOKE, [&](STObject& obj) {}); + + { + // Cached generation value + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + StubHookContext stubCtx{.generation = 4}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.etxn_generation() == 5); + } + + { + // No emit details -> otxn_generation() == 0 + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.etxn_generation() == 1); + } + + { + // Emit details supply generation + STTx emitTx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails) + .setFieldU32(sfEmitGeneration, 2); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, emitTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.etxn_generation() == 3); + } + } + + void + test_etxn_nonce(FeatureBitset features) + { + testcase("Test etxn_nonce"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + hook::HookContext hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .expected_etxn_count = -1, + .nonce_used = {{uint256(0), true}}, + .result = {.hookCanEmit = uint256()}, + }); + + // TOO_MANY_NONCES + { + hookCtx.emit_nonce_counter = hook_api::max_nonce + 1; + auto& api = hookCtx.api(); + auto const result = api.etxn_nonce(); + BEAST_EXPECT(result.error() == TOO_MANY_NONCES); + } + + // SUCCESS + { + hookCtx.emit_nonce_counter = hook_api::max_nonce; + auto& api = hookCtx.api(); + auto const result = api.etxn_nonce(); + BEAST_EXPECT(result.has_value()); + } + + { + // Flags and cache tracking + StubHookContext stubCtx{ + .nonce_used = {}, + .result = + {.isCallback = true, + .isStrong = true, + .hookChainPosition = 2}, + }; + auto hookCtxNonce = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + hook::HookAPI api(hookCtxNonce); + auto const expectedFlags = + static_cast(0b11U | (2U << 2U)); + auto const expected = ripple::sha512Half( + ripple::HashPrefix::emitTxnNonce, + hookCtxNonce.applyCtx.tx.getTransactionID(), + hookCtxNonce.emit_nonce_counter, + hookCtxNonce.result.account, + hookCtxNonce.result.hookHash, + expectedFlags); + auto const result = api.etxn_nonce(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expected); + BEAST_EXPECT(hookCtxNonce.emit_nonce_counter == 1); + BEAST_EXPECT(hookCtxNonce.nonce_used.count(expected) == 1); + } + } + + void + test_etxn_reserve(FeatureBitset features) + { + testcase("Test etxn_reserve"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // ALREADY_SET + StubHookContext stubCtx{.expected_etxn_count = 1}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.etxn_reserve(2); + BEAST_EXPECT(result.error() == ALREADY_SET); + } + + { + // TOO_SMALL + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.etxn_reserve(0); + BEAST_EXPECT(result.error() == TOO_SMALL); + } + + { + // TOO_BIG + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.etxn_reserve(hook_api::max_emit + 1); + BEAST_EXPECT(result.error() == TOO_BIG); + } + + { + // SUCCESS + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.etxn_reserve(3); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(hookCtx.expected_etxn_count == 3); + } + } + + void + test_fee_base(FeatureBitset features) + { + testcase("Test fee_base"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const expected = env.closed()->fees().base.drops(); + BEAST_EXPECT(api.fee_base() == expected); + } + + void + ASSERT_FLOAT_EQUAL(hook::HookAPI& api, uint64_t x, uint64_t y) + { + auto float_exponent = [](uint64_t f) -> int32_t { + return ((int32_t)(((f) >> 54U) & 0xFFU)) - 97; + }; + int64_t px = (x); + int64_t py = (y); + int64_t mx = api.float_mantissa(px).value(); + int64_t my = api.float_mantissa(py).value(); + int32_t diffexp = float_exponent(px) - float_exponent(py); + if (diffexp == 1) + mx *= 10LL; + if (diffexp == -1) + my *= 10LL; + int64_t diffman = mx - my; + if (diffman < 0) + diffman *= -1LL; + if (diffexp < 0) + diffexp *= -1; + if (diffexp > 1 || diffman > 5000000 || mx < 0 || my < 0) + BEAST_EXPECT(false); + else + BEAST_EXPECT(true); + } + + void + test_float_compare(FeatureBitset features) + { + testcase("Test float_compare"); + + using namespace jtx; + using namespace hook_api; + using namespace compare_mode; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const testSuccess = + [&](uint64_t left, uint64_t right, uint32_t mode) { + auto const result = api.float_compare(left, right, mode); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + }; + + auto const testError = [&](uint64_t left, + uint64_t right, + uint32_t mode, + hook::HookReturnCode error) { + auto const result = api.float_compare(left, right, mode); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == error); + }; + + auto const one = api.float_one(); + auto const two = api.float_set(0, 2).value(); + auto const three = 6091866696204910592ULL; // encoded 3.0 from SetHook + + testSuccess(one, one, EQUAL); + testSuccess(one, two, LESS); + testSuccess(two, one, GREATER); + testError(one, two, 0, INVALID_ARGUMENT); + + // Invalid flags + testError(one, two, 0b1000U, INVALID_ARGUMENT); + testError(one, two, ~0b111U, INVALID_ARGUMENT); + testError(one, two, 0b111U, INVALID_ARGUMENT); + + // Relative ordering samples + uint64_t largeNegative = 1622844335003378560ULL; // -154846915 + uint64_t smallNegative = 1352229899321148800ULL; // -1.15001111e-7 + uint64_t smallPositive = + 5713898440837102138ULL; // 3.33411333131321e-21 + uint64_t largePositive = 7749425685711506120ULL; // 3.234326634253e+92 + + testSuccess(largeNegative, smallNegative, LESS); + testSuccess(largeNegative, largePositive, LESS); + testSuccess(smallNegative, smallPositive, LESS); + testSuccess(smallPositive, largePositive, LESS); + testSuccess(smallNegative, 0, LESS); + testSuccess(largeNegative, 0, LESS); + testSuccess(smallPositive, 0, GREATER); + testSuccess(largePositive, 0, GREATER); + + // Not-equal flag check + testSuccess(two, three, GREATER | LESS); + } + + void + test_float_divide(FeatureBitset features) + { + testcase("Test float_divide"); + + using namespace jtx; + using namespace hook_api; + using namespace compare_mode; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + + // divide by 0 + BEAST_EXPECT(api.float_divide(one, 0).error() == DIVISION_BY_ZERO); + BEAST_EXPECT(api.float_divide(0, one).value() == 0); + + // check 1 + BEAST_EXPECT(api.float_divide(one, one).value() == one); + BEAST_EXPECT( + api.float_divide(one, api.float_negate(one)).value() == + api.float_negate(one)); + BEAST_EXPECT( + api.float_divide(api.float_negate(one), one).value() == + api.float_negate(one)); + BEAST_EXPECT( + api.float_divide(api.float_negate(one), api.float_negate(one)) + .value() == one); + + // 1 / 10 = 0.1 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(one, 6107881094714392576LL).value(), + 6071852297695428608LL); + + // 123456789 / 1623 = 76067.0295749 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(6234216452170766464LL, 6144532891733356544LL) + .value(), + 6168530993200328528LL); + + // -1.245678451111 / 1.3546984132111e+42 = -9.195245517106014e-43 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(1478426356228633688LL, 6846826132016365020LL) + .value(), + 711756787386903390LL); + + // 9.134546514878452e-81 / 1 + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(4638834963451748340LL, one).value(), + 4638834963451748340LL); + + // 9.134546514878452e-81 / 1.41649684651e+75 = (underflow 0) + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(4638834963451748340LL, 7441363081262569392LL) + .value(), + 0); + + // 1.3546984132111e+42 / 9.134546514878452e-81 = XFL_OVERFLOW + BEAST_EXPECT( + api.float_divide(6846826132016365020LL, 4638834963451748340LL) + .error() == XFL_OVERFLOW); + + // clang-format off + std::vector> tests = { + {3121244226425810900LL /* -4.753284285427668e+91 */, 2135203055881892282LL /* -9.50403176301817e+36 */, 7066645550312560102LL /* 5.001334595622374e+54 */}, + {2473507938381460320LL /* -5.535342582428512e+55 */, 6365869885731270068LL /* 6787211884129716 */ , 2187897766692155363LL /* -8.155547044835299e+39 */}, + {1716271542690607496LL /* -49036842898190.16 */, 3137794549622534856LL /* -3.28920897266964e+92 */, 4667220053951274769LL /* 1.490839995440913e-79 */}, + {1588045991926420391LL /* -2778923.092005799 */, 5933338827267685794LL /* 6.601717648113058e-9 */, 1733591650950017206LL /* -420939403974674.2 */}, + {5880783758174228306LL /* 8.089844083101523e-12 */, 1396720886139976383LL /* -0.00009612200909863615 */, 1341481714205255877LL /* -8.416224503589061e-8 */}, + {5567703563029955929LL /* 1.254423600022873e-29 */, 2184969513100691140LL /* -5.227293453371076e+39 */, 236586937995245543LL /* -2.399757371979751e-69 */}, + {7333313065548121054LL /* 1.452872188953566e+69 */, 1755926008837497886LL /* -8529353417745438 */, 2433647177826281173LL /* -1.703379046213333e+53 */}, + {1172441975040622050LL /* -1.50607192429309e-17 */, 6692015311011173216LL /* 8.673463993357152e+33 */, 560182767210134346LL /* -1.736413416192842e-51 */}, + {577964843368607493LL /* -1.504091065184005e-50 */, 6422931182144699580LL /* 9805312769113276000 */, 235721135837751035LL /* -1.533955214485243e-69 */}, + {6039815413139899240LL /* 0.0049919124634346 */, 2117655488444284242LL /* -9.970862834892113e+35 */, 779625635892827768LL /* -5.006499985102456e-39 */}, + {1353563835098586141LL /* -2.483946887437341e-7 */, 6450909070545770298LL /* 175440415122002600000 */, 992207753070525611LL /* -1.415835049016491e-27 */}, + {6382158843584616121LL /* 50617712279937850 */, 5373794957212741595LL /* 5.504201387110363e-40 */, 7088854809772330055LL /* 9.196195545910343e+55 */}, + {2056891719200540975LL /* -3.250289119594799e+32 */, 1754532627802542730LL /* -7135972382790282 */, 6381651867337939070LL /* 45547949813167340 */}, + {5730152450208688630LL /* 1.573724193417718e-20 */, 1663581695074866883LL /* -62570322025.24355 */, 921249452789827075LL /* -2.515128806245891e-31 */}, + {6234301156018475310LL /* 131927173.7708846 */, 2868710604383082256LL /* -4.4212413754468e+77 */, 219156721749007916LL /* -2.983939635224108e-70 */}, + {2691125731495874243LL /* -6.980353583058627e+67 */, 7394070851520237320LL /* 8.16746263262388e+72 */, 1377640825464715759LL /* -0.000008546538744084975 */}, + {5141867696142208039LL /* 7.764120939842599e-53 */, 5369434678231981897LL /* 1.143922406350665e-40 */, 5861466794943198400LL /* 6.7872793615536e-13 */}, + {638296190872832492LL /* -7.792243040963052e-47 */, 5161669734904371378LL /* 9.551761192523954e-52 */, 1557396184145861422LL /* -81579.12330410798 */}, + {2000727145906286285LL /* -1.128911353786061e+29 */, 2096625200460673392LL /* -6.954973360763248e+34 */, 5982403476503576795LL /* 0.000001623171355558107 */}, + {640472838055334326LL /* -9.968890223464885e-47 */, 5189754252349396763LL /* 1.607481618585371e-50 */, 1537425431139169736LL /* -6201.557833201096 */}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_divide(std::get<0>(test), std::get<1>(test)).value(), + std::get<2>(test)); + } + } + + void + test_float_int(FeatureBitset features) + { + testcase("Test float_int"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + + // clang-format off + + // check 1 + BEAST_EXPECT(api.float_int(one, 0, 0).value() == 1LL); + + // check 1.23e-20 always returns 0 (too small to display) + BEAST_EXPECT(api.float_int(5729808726015270912LL, 0, 0).value() == 0); + BEAST_EXPECT(api.float_int(5729808726015270912LL, 15, 0).value() == 0); + BEAST_EXPECT( + api.float_int(5729808726015270912LL, 16, 0).error() == + INVALID_ARGUMENT); + + BEAST_EXPECT(api.float_int(one, 15, 0).value() == 1000000000000000LL); + BEAST_EXPECT(api.float_int(one, 14, 0).value() == 100000000000000LL); + BEAST_EXPECT(api.float_int(one, 13, 0).value() == 10000000000000LL); + BEAST_EXPECT(api.float_int(one, 12, 0).value() == 1000000000000LL); + BEAST_EXPECT(api.float_int(one, 11, 0).value() == 100000000000LL); + BEAST_EXPECT(api.float_int(one, 10, 0).value() == 10000000000LL); + BEAST_EXPECT(api.float_int(one, 9, 0).value() == 1000000000LL); + BEAST_EXPECT(api.float_int(one, 8, 0).value() == 100000000LL); + BEAST_EXPECT(api.float_int(one, 7, 0).value() == 10000000LL); + BEAST_EXPECT(api.float_int(one, 6, 0).value() == 1000000LL); + BEAST_EXPECT(api.float_int(one, 5, 0).value() == 100000LL); + BEAST_EXPECT(api.float_int(one, 4, 0).value() == 10000LL); + BEAST_EXPECT(api.float_int(one, 3, 0).value() == 1000LL); + BEAST_EXPECT(api.float_int(one, 2, 0).value() == 100LL); + BEAST_EXPECT(api.float_int(one, 1, 0).value() == 10LL); + BEAST_EXPECT(api.float_int(one, 0, 0).value() == 1LL); + + // normal upper limit on exponent + BEAST_EXPECT(api.float_int(6360317241828374919LL, 0, 0).value() == 1234567981234567LL); + + // ask for one decimal above limit + BEAST_EXPECT(api.float_int(6360317241828374919LL, 1, 0).error() == TOO_BIG); + + // ask for 15 decimals above limit + BEAST_EXPECT(api.float_int(6360317241828374919LL, 15, 0).error() == TOO_BIG); + + // every combination for 1.234567981234567 + BEAST_EXPECT(api.float_int(6090101264186145159LL, 0, 0).value() == 1LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 1, 0).value() == 12LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 2, 0).value() == 123LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 3, 0).value() == 1234LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 4, 0).value() == 12345LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 5, 0).value() == 123456LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 6, 0).value() == 1234567LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 7, 0).value() == 12345679LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 8, 0).value() == 123456798LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 9, 0).value() == 1234567981LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 10, 0).value() == 12345679812LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 11, 0).value() == 123456798123LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 12, 0).value() == 1234567981234LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 13, 0).value() == 12345679812345LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 14, 0).value() == 123456798123456LL); + BEAST_EXPECT(api.float_int(6090101264186145159LL, 15, 0).value() == 1234567981234567LL); + + // same with absolute parameter + BEAST_EXPECT(api.float_int(1478415245758757255LL, 0, 1) .value() ==1LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 1, 1) .value() ==12LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 2, 1) .value() ==123LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 3, 1) .value() ==1234LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 4, 1) .value() ==12345LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 5, 1) .value() ==123456LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 6, 1) .value() ==1234567LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 7, 1) .value() ==12345679LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 8, 1) .value() ==123456798LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 9, 1) .value() ==1234567981LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 10, 1).value() == 12345679812LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 11, 1).value() == 123456798123LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 12, 1).value() == 1234567981234LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 13, 1).value() == 12345679812345LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 14, 1).value() == 123456798123456LL); + BEAST_EXPECT(api.float_int(1478415245758757255LL, 15, 1).value() == 1234567981234567LL); + + // neg xfl sans absolute parameter + BEAST_EXPECT(api.float_int(1478415245758757255LL, 15, 0).error() == CANT_RETURN_NEGATIVE); + + // 1.234567981234567e-16 + BEAST_EXPECT(api.float_int(5819885286543915399LL, 15, 0).value() == 1LL); + for (uint32_t i = 1; i < 15; ++i) + BEAST_EXPECT(api.float_int(5819885286543915399LL, i, 0).value() == 0); + + // clang-format on + } + + void + test_float_invert(FeatureBitset features) + { + testcase("Test float_invert"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // divide by 0 + BEAST_EXPECT(api.float_invert(0).error() == DIVISION_BY_ZERO); + + // check 1 + BEAST_EXPECT( + api.float_invert(api.float_one()).value() == api.float_one()); + + // 1 / 10 = 0.1 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(6107881094714392576LL).value(), + 6071852297695428608LL); + + // 1 / 123 = 0.008130081300813009 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(6126125493223874560LL).value(), + 6042953581977277649LL); + + // 1 / 1234567899999999 = 8.100000008100007e-16 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(6360317241747140351LL).value(), + 5808736320061298855LL); + + // 1/ 1*10^-81 = 10**81 + ASSERT_FLOAT_EQUAL( + api, + api.float_invert(4630700416936869888LL).value(), + 7540018576963469311LL); + } + + void + test_float_log(FeatureBitset features) + { + testcase("Test float_log"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // check 0 is not allowed + BEAST_EXPECT(api.float_log(0).error() == INVALID_ARGUMENT); + + // log10( 846513684968451 ) = 14.92763398342338 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(6349533412187342878LL).value(), + 6108373858112734914LL); + + // log10 ( -1000 ) = invalid (complex not supported) + BEAST_EXPECT( + api.float_log(1532223873305968640LL).error() == + COMPLEX_NOT_SUPPORTED); + + // log10 (1000) == 3 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(6143909891733356544LL).value(), + 6091866696204910592LL); + + // log10 (0.112381) == -0.949307107740766 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(6071976107695428608LL).value(), + 1468659350345448364LL); + + // log10 (0.00000000000000001123) = -16.94962024373854221 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(5783744921543716864LL).value(), + 1496890038311378526LL); + + // log10(100000000000000000000000000000000000000000000000000000000000000) + // = 62 + ASSERT_FLOAT_EQUAL( + api, + api.float_log(7206759403792793600LL).value(), + 6113081094714392576LL); + } + + void + test_float_mantissa(FeatureBitset features) + { + testcase("Test float_mantissa"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // test canonical zero + BEAST_EXPECT(api.float_mantissa(0).value() == 0); + + // test one, negative one + { + BEAST_EXPECT( + api.float_mantissa(api.float_one()).value() == + 1000000000000000LL); + BEAST_EXPECT( + api.float_mantissa(api.float_negate(api.float_one())).value() == + 1000000000000000LL); + } + + // test random numbers + { + // clang-format off + std::vector> tests = { + {4763370308433150973LL /* 7.569101929907197e-74 */, 7569101929907197LL}, + {668909658849475214LL /* -2.376913998641806e-45 */, 2376913998641806LL}, + {962271544155031248LL /* -7.508423152486096e-29 */, 7508423152486096LL}, + {7335644976228470276LL /* 3.784782869302788e+69 */, 3784782869302788LL}, + {2837780149340315954LL /* -9.519583351644467e+75 */, 9519583351644466LL}, + {2614004940018599738LL /* -1.917156143712058e+63 */, 1917156143712058LL}, + {4812250541755005603LL /* 2.406139723315875e-71 */, 2406139723315875LL}, + {5140304866732560580LL /* 6.20129153019514e-53 */, 6201291530195140LL}, + {1124677839589482624LL /* -7.785132001599617e-20 */, 7785132001599616LL}, + {5269336076015865585LL /* 9.131711247126257e-46 */, 9131711247126257LL}, + {2296179634826760368LL /* -8.3510241225484e+45 */, 8351024122548400LL}, + {1104028240398536470LL /* -5.149931320135446e-21 */, 5149931320135446LL}, + {2691222059222981864LL /* -7.076681310166248e+67 */, 7076681310166248LL}, + {6113256168823855946LL /* 63.7507410946337 */, 6375074109463370LL}, + {311682216630003626LL /* -5.437441968809898e-65 */, 5437441968809898LL}, + {794955605753965262LL /* -2.322071336757966e-38 */, 2322071336757966LL}, + {204540636400815950LL /* -6.382252796514126e-71 */, 6382252796514126LL}, + {5497195278343034975LL /* 2.803732951029855e-33 */, 2803732951029855LL}, + {1450265914369875626LL /* -0.09114033611316906 */, 9114033611316906LL}, + {7481064015089962668LL /* 5.088633654939308e+77 */, 5088633654939308LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_mantissa(std::get<0>(test)).value(), + std::get<1>(test)); + } + } + } + + void + test_float_mulratio(FeatureBitset features) + { + testcase("Test float_mulratio"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + auto const neg_one = api.float_negate(one); + + // multiply by 0 + BEAST_EXPECT(api.float_mulratio(one, 0, 0, 1).value() == 0); + BEAST_EXPECT(api.float_mulratio(0, 0, 1, 1).value() == 0); + + // check 1 + BEAST_EXPECT(api.float_mulratio(one, 0, 1, 1).value() == one); + BEAST_EXPECT(api.float_mulratio(neg_one, 0, 1, 1).value() == neg_one); + + // check overflow + // 1e+95 * 1e+95 + BEAST_EXPECT( + api.float_mulratio(7801234554605699072LL, 0, 0xFFFFFFFFUL, 1) + .error() == XFL_OVERFLOW); + // 1e+95 * 10 + BEAST_EXPECT( + api.float_mulratio(7801234554605699072LL, 0, 10, 1).error() == + XFL_OVERFLOW); + // -1e+95 * 10 + BEAST_EXPECT( + api.float_mulratio(3189548536178311168LL, 0, 10, 1).error() == + XFL_OVERFLOW); + + // identity + ASSERT_FLOAT_EQUAL( + api, + api.float_mulratio(3189548536178311168LL, 0, 1, 1).value(), + 3189548536178311168LL); + + // random mulratios + + // clang-format off + std::vector> tests = { + {2296131684119423544LL, 2210828011U, 2814367554U, 2294351094683836182LL}, + {565488225163275031LL, 2373474507U, 4203973264U, 562422045628095449LL}, + {2292703263479286183LL, 3170020147U, 773892643U, 2307839765178024100LL}, + {758435948837102675LL, 3802740780U, 1954123588U, 760168290112163547LL}, + {3063742137774439410LL, 2888815591U, 4122448592U, 3053503824756415637LL}, + {974014561126802184LL, 689168634U, 3222648522U, 957408554638995792LL}, + {2978333847445611553LL, 1718558513U, 2767410870U, 2976075722223325259LL}, + {6577058837932757648LL, 1423256719U, 1338068927U, 6577173649752398013LL}, + {2668681541248816636LL, 345215754U, 4259223936U, 2650183845127530219LL}, + {651803640367065917LL, 327563234U, 1191613855U, 639534906402789368LL}, + {3154958130393015979LL, 1304112625U, 3024066701U, 3153571282364880740LL}, + {1713286099776800976LL, 1902151138U, 2927030061U, 1712614441093927706LL}, + {2333142120591277120LL, 914099656U, 108514965U, 2349692988167140475LL}, + {995968561418010814LL, 1334462574U, 846156977U, 998955931389416094LL}, + {6276035843030312442LL, 2660687613U, 236740983U, 6294920527635363073LL}, + {7333118474702086419LL, 46947714U, 2479204760U, 7298214153648998535LL}, + {2873297486994296492LL, 880591893U, 436034100U, 2884122995598532757LL}, + {1935815261812737573LL, 3123665800U, 3786746543U, 1934366328810191207LL}, + {7249556282125616118LL, 2378803159U, 2248850590U, 7250005170160875417LL}, + {311005347529659996LL, 992915590U, 2433548552U, 308187142737041830LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_mulratio( + std::get<0>(test), + 0U, + std::get<1>(test), + std::get<2>(test)) + .value(), + std::get<3>(test)); + } + } + + void + test_float_multiply(FeatureBitset features) + { + testcase("Test float_multiply"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + auto const neg_one = api.float_negate(one); + + // multiply by 0 + BEAST_EXPECT(api.float_multiply(one, 0).value() == 0); + BEAST_EXPECT(api.float_multiply(0, one).value() == 0); + + // check 1 + BEAST_EXPECT(api.float_multiply(one, one).value() == one); + BEAST_EXPECT(api.float_multiply(one, neg_one).value() == neg_one); + BEAST_EXPECT(api.float_multiply(neg_one, one).value() == neg_one); + BEAST_EXPECT(api.float_multiply(neg_one, neg_one).value() == one); + + // check overflow + // 1e+95 * 1e+95 + BEAST_EXPECT( + api.float_multiply(7801234554605699072LL, 7801234554605699072LL) + .error() == XFL_OVERFLOW); + // 1e+95 * 10 + BEAST_EXPECT( + api.float_multiply(7801234554605699072LL, 6107881094714392576LL) + .error() == XFL_OVERFLOW); + BEAST_EXPECT( + api.float_multiply(6107881094714392576LL, 7801234554605699072LL) + .error() == XFL_OVERFLOW); + // -1e+95 * 10 + BEAST_EXPECT( + api.float_multiply(3189548536178311168LL, 6107881094714392576LL) + .error() == XFL_OVERFLOW); + + // identity + ASSERT_FLOAT_EQUAL( + api, + api.float_multiply(3189548536178311168LL, one).value(), + 3189548536178311168LL); + ASSERT_FLOAT_EQUAL( + api, + api.float_multiply(one, 3189548536178311168LL).value(), + 3189548536178311168LL); + + // random multiplications + + // clang-format off + std::vector> tests = { + {7791757438262485039LL /* 9.537282166267951e+94 */, 4759088999670263908LL /* 3.287793167020132e-74 */, 6470304726017852129LL /* 3.135661113819873e+21 */}, + {7534790022873909775LL /* 4.771445910440463e+80 */, 1017891960669847079LL /* -9.085644138855975e-26 */, 2472307761756037979LL /* -4.335165957006171e+55 */}, + {2813999069907898454LL /* -3.75290242870895e+74 */, 4962524721184225460LL /* 8.56513107667986e-63 */, 1696567870013294731LL /* -3214410121988.235 */}, + {2151742066453140308LL /* -8.028643824784212e+37 */, 437647738130579252LL /* -5.302173903011636e-58 */, 5732835652591705549LL /* 4.256926576434637e-20 */}, + {5445302332922546340LL /* 4.953983058987172e-36 */, 7770966530708354172LL /* 6.760773121619068e+93 */, 7137051085305881332LL /* 3.349275551015668e+58 */}, + {2542989542826132533LL /* -2.959352989172789e+59 */, 6308418769944702613LL /* 3379291626008.213 */, 2775217422137696934LL /* -1.000051677471398e+72 */}, + {5017652318929433511LL /* 9.649533293441959e-60 */, 6601401767766764916LL /* 8.131913296358772e+28 */, 5538267259220228820LL /* 7.846916809259732e-31 */}, + {892430323307269235LL /* -9.724796342652019e-33 */, 1444078017997143500LL /* -0.0292613723858478 */, 5479222755754111850LL /* 2.845608871588714e-34 */}, + {7030632722283214253LL /* 5.017303585240493e+52 */, 297400838197636668LL /* -9.170462045924924e-66 */, 1247594596364389994LL /* -4.601099210133098e-13 */}, + {1321751204165279730LL /* -6.700112973094898e-9 */, 2451801790748530375LL /* -1.843593458980551e+54 */, 6918764256086244704LL /* 1.235228445162848e+46 */}, + {2055496484261758590LL /* -1.855054180812414e+32 */, 2079877890137711361LL /* -8.222061547283201e+33 */, 7279342234795540005LL /* 1.525236964818469e+66 */}, + {2439875962311968674LL /* -7.932163531900834e+53 */, 4707485682591872793LL /* 5.727671617074969e-77 */, 1067392794851803610LL /* -4.543282792366554e-23 */}, + {6348574818322812800LL /* 750654298515443.2 */, 6474046245013515838LL /* 6.877180109483582e+21 */, 6742547427357110773LL /* 5.162384810848757e+36 */}, + {1156137305783593424LL /* -3.215801176746448e-18 */, 351790564990861307LL /* -9.516993310703611e-63 */, 4650775291275116747LL /* 3.060475828764875e-80 */}, + {5786888485280994123LL /* 4.266563737277259e-17 */, 6252137323085080394LL /* 1141040294.831946 */, 5949619829273756852LL /* 4.868321144702132e-8 */}, + {2078182880999439640LL /* -6.52705240901148e+33 */, 1662438186251269392LL /* -51135233789.26864 */, 6884837854131013998LL /* 3.33762350889611e+44 */}, + {1823781083140711248LL /* -43268336830308640000 */, 1120252241608199010LL /* -3.359534020316002e-20 */, 6090320310700749729LL /* 1.453614495839137 */}, + {6617782604883935174LL /* 6.498351904047046e+29 */, 6185835042802056262LL /* 689635.404973575 */, 6723852137583788319LL /* 4.481493547008287e+35 */}, + {333952667495151166LL /* -9.693494324475454e-64 */, 1556040883317758614LL /* -68026.1150230799 */, 5032611291744396930LL /* 6.594107598923394e-59 */}, + {2326968399632616779LL /* -3.110991909440843e+47 */, 707513695207834635LL /* -4.952153338037259e-43 */, 6180479299649214949LL /* 154061.0896894437 */}, + {1271003508324696477LL /* -9.995612660957597e-12 */, 5321949753651889765LL /* 7.702193354704484e-43 */, 512101972406838314LL /* -7.698814141342762e-54 */}, + {1928646740923345323LL /* -1.106100408773035e+25 */, 4639329980209973352LL /* 9.629563273103463e-81 */, 487453886143282122LL /* -1.065126387268554e-55 */}, + {6023906813956669432LL /* 0.0007097711789686777 */, 944348444470060009LL /* -7.599721976996842e-30 */, 888099590592064434LL /* -5.394063627447218e-33 */}, + {6580290597764062787LL /* 5.035141803138627e+27 */, 6164319297265300034LL /* 33950.07022461506 */, 6667036882686408593LL /* 1.709434178074513e+32 */}, + {2523439530503240484LL /* -1.423739175762724e+58 */, 5864448766677980801LL /* 9.769251096336e-13 */, 2307233895764065602LL /* -1.39088655037165e+46 */}, + {6760707453987140465LL /* 5.308012931396465e+37 */, 5951641080643457645LL /* 6.889572514402925e-8 */, 6632955645489194550LL /* 3.656993999824438e+30 */}, + {6494270716308443375LL /* 9.087252894929135e+22 */, 564752637895553836LL /* -6.306284101612332e-51 */, 978508199357889360LL /* -5.730679845862224e-28 */}, + {6759145618427534062LL /* 3.746177371790062e+37 */, 4721897842483633304LL /* 2.125432999353496e-76 */, 5394267403342547165LL /* 7.962249007433949e-39 */}, + {1232673571201806425LL /* -7.694472557031513e-14 */, 6884256144221925318LL /* 2.75591359980743e+44 */, 2037747561727791012LL /* -2.12053015632682e+31 */}, + {1427694775835421031LL /* -0.004557293586344295 */, 4883952867277976402LL /* 2.050871208358738e-67 */, 225519204318055258LL /* -9.34642220427145e-70 */}, + {5843509949864662087LL /* 6.84483279249927e-14 */, 5264483986612843822LL /* 4.279621844104494e-46 */, 5028946513739275800LL /* 2.929329593802264e-59 */}, + {6038444022009738988LL /* 0.003620521333274348 */, 7447499078040748850LL /* 7.552493624689458e+75 */, 7406652183825856093LL /* 2.734396428760669e+73 */}, + {939565473697468970LL /* -2.816751204405802e-30 */, 1100284903077087966LL /* -1.406593998686942e-21 */, 5174094397561240825LL /* 3.962025339911417e-51 */}, + {5694071830210473617LL /* 1.521901214166673e-22 */, 5536709154363579683LL /* 6.288811952610595e-31 */, 5143674525748709391LL /* 9.570950546343951e-53 */}, + {600729862341871819LL /* -6.254711528966347e-49 */, 6330630279715378440LL /* 75764028872020.56 */, 851415551394320910LL /* -4.738821448667662e-35 */}, + {1876763139233864902LL /* -3.265694247738566e+22 */, 4849561230315278754LL /* 3.688031264625058e-69 */, 649722744589988028LL /* -1.204398248636604e-46 */}, + {3011947542126279863LL /* -3.542991042788535e+85 */, 1557732559110376235LL /* -84942.87294925611 */,7713172080438368541LL /* 3.009518380079389e+90 */}, + {5391579936313268788LL /* 5.274781978155572e-39 */, 1018647290024655822LL /* -9.840973493664718e-26 */, 329450072133864644LL /* -5.190898963188932e-64 */}, + {2815029221608845312LL /* -4.783054129655808e+74 */, 4943518985822088837LL /* 7.57379422402522e-64 */,1678961648155863225LL /* -362258677403.8713 */ }, + {1377509900308195934LL /* -0.00000841561358756515 */, 7702104197062186199LL /* 9.95603351337903e+89 */, 2998768765665354000LL /* -8.378613091344656e+84 */}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_multiply(std::get<0>(test), std::get<1>(test)) + .value(), + std::get<2>(test)); + } + } + + void + test_float_negate(FeatureBitset features) + { + testcase("Test float_negate"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + + // test canonical zero + BEAST_EXPECT(api.float_negate(0) == 0); + + // test double negation + { + BEAST_EXPECT(api.float_negate(one) != one); + BEAST_EXPECT(api.float_negate(api.float_negate(one)) == one); + } + + // test random numbers + { + // +/- 3.463476342523e+22 + BEAST_EXPECT( + api.float_negate(6488646939756037240LL) == + 1876960921328649336LL); + + BEAST_EXPECT(api.float_negate(one) == 1478180677777522688LL); + + BEAST_EXPECT( + api.float_negate(1838620299498162368LL) == + 6450306317925550272LL); + } + } + + void + test_float_one(FeatureBitset features) + { + testcase("Test float_one"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + BEAST_EXPECT(one == 6089866696204910592ULL); + } + + void + test_float_root(FeatureBitset features) + { + testcase("Test float_root"); + + using namespace jtx; + using namespace hook_api; + using namespace compare_mode; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + + // sqrt 1 is 1 + ASSERT_FLOAT_EQUAL(api, api.float_root(one, 2).value(), one); + + // sqrt 9 is 3 + auto const nine = 6097866696204910592LL; + auto const three = 6091866696204910592LL; + ASSERT_FLOAT_EQUAL(api, api.float_root(nine, 2).value(), three); + + // cube root of 1000 is 10 + auto const thousand = 6143909891733356544LL; + auto const ten = 6107881094714392576LL; + ASSERT_FLOAT_EQUAL(api, api.float_root(thousand, 3).value(), ten); + + // sqrt of negative is "complex not supported error" + auto const negative_one = 1478180677777522688LL; + BEAST_EXPECT( + api.float_root(negative_one, 2).error() == COMPLEX_NOT_SUPPORTED); + + // tenth root of 0 is 0 + ASSERT_FLOAT_EQUAL(api, api.float_root(0, 10).value(), 0); + } + + void + test_float_set(FeatureBitset features) + { + testcase("Test float_set"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // zero mantissa should return canonical zero + BEAST_EXPECT(api.float_set(-5, 0).value() == 0); + BEAST_EXPECT(api.float_set(50, 0).value() == 0); + BEAST_EXPECT(api.float_set(-50, 0).value() == 0); + BEAST_EXPECT(api.float_set(0, 0).value() == 0); + + // an exponent lower than -96 should produce an invalid float error + BEAST_EXPECT(api.float_set(-97, 1).error() == INVALID_FLOAT); + + // an exponent larger than +96 should produce an invalid float error + BEAST_EXPECT(api.float_set(+97, 1).error() == INVALID_FLOAT); + + // clang-format off + std::vector> tests = { + {-5, 6541432897943971LL, 6275552114197674403LL}, + {-83, 7906202688397446LL, 4871793800248533126LL}, + {76, 4760131426754533LL, 7732937091994525669LL}, + {37, -8019384286534438LL, 2421948784557120294LL}, + {50, 5145342538007840LL, 7264947941859247392LL}, + {-70, 4387341302202416LL, 5102462119485603888LL}, + {-26, -1754544005819476LL, 1280776838179040340LL}, + {36, 8261761545780560LL, 7015862781734272336LL}, + {35, 7975622850695472LL, 6997562244529705264LL}, + {17, -4478222822793996LL, 2058119652903740172LL}, + {-53, 5506604247857835LL, 5409826157092453035LL}, + {-60, 5120164869507050LL, 5283338928147728362LL}, + {41, 5176113875683063LL, 7102849126611584759LL}, + {-54, -3477931844992923LL, 778097067752718235LL}, + {21, 6345031894305479LL, 6743730074440567495LL}, + {-23, 5091583691147091LL, 5949843091820201811LL}, + {-33, 7509684078851678LL, 5772117207113086558LL}, + {-72, -1847771838890268LL, 452207734575939868LL}, + {71, -9138413713437220LL, 3035557363306410532LL}, + {28, 4933894067102586LL, 6868419726179738490LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_set(std::get<0>(test), std::get<1>(test)).value(), + std::get<2>(test)); + } + } + + void + test_float_sign(FeatureBitset features) + { + testcase("Test float_sign"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // test canonical zero + BEAST_EXPECT(api.float_sign(0) == 0); + + // test one + auto const one = api.float_one(); + BEAST_EXPECT(api.float_sign(one) == 0); + BEAST_EXPECT(api.float_sign(api.float_negate(one)) == 1); + + // test random numbers + + // clang-format off + std::vector> tests = { + {7248434512952957686LL /* 6.646312141200119e+64 */, 0LL}, + {889927818394811978LL /* -7.222291430194763e-33 */, 1LL}, + {5945816149233111421LL /* 1.064641104056701e-8 */, 0LL}, + {6239200145838704863LL /* 621826155.7938399 */, 0LL}, + {6992780785042190360LL /* 3.194163363180568e+50 */, 0LL}, + {6883099933108789087LL /* 1.599702486671199e+44 */, 0LL}, + {890203738162163464LL /* -7.498211197546248e-33 */, 1LL}, + {4884803073052080964LL /* 2.9010769824633e-67 */, 0LL}, + {2688292350356944394LL /* -4.146972444128778e+67 */, 1LL}, + {4830109852288093280LL /* 2.251051746921568e-70 */, 0LL}, + {294175951907940320LL /* -5.945575756228576e-66 */, 1LL}, + {7612037404955382316LL /* 9.961233953985069e+84 */, 0LL}, + {7520840929603658997LL /* 8.83675114967167e+79 */, 0LL}, + {4798982086157926282LL /* 7.152082635718538e-72 */, 0LL}, + {689790136568817905LL /* -5.242993208502513e-44 */, 1LL}, + {5521738045011558042LL /* 9.332101110070938e-32 */, 0LL}, + {728760820583452906LL /* -8.184880204173546e-42 */, 1LL}, + {2272937984362856794LL /* -3.12377216812681e+44 */, 1LL}, + {1445723661896317830LL /* -0.0457178113775911 */, 1LL}, + {5035721527359772724LL /* 9.704343214299189e-59 */, 0LL}, + }; + // clang-format on + + for (auto const& test : tests) + { + BEAST_EXPECT( + api.float_sign(std::get<0>(test)) == std::get<1>(test)); + } + } + + void + test_float_sto(FeatureBitset features) + { + testcase("Test float_sto"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + uint64_t const xfl = 6198187654261802496ULL; // 1234567.0 as in SetHook + // XRP serialization + auto const ser = api.float_sto(std::nullopt, std::nullopt, xfl, 0, 8); + BEAST_EXPECT(ser.has_value()); + auto const parsed = api.float_sto_set(ser.value()); + BEAST_EXPECT(parsed.has_value()); + BEAST_EXPECT( + api.float_compare(parsed.value(), xfl, compare_mode::EQUAL) + .value() == 1); + } + + void + test_float_sto_set(FeatureBitset features) + { + testcase("Test float_sto_set"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // Not enough bytes + BEAST_EXPECT( + api.float_sto_set(Bytes{1, 2, 3}).error() == NOT_AN_OBJECT); + + uint64_t const xfl = 6198187654261802496ULL; // 1234567.0 as in SetHook + auto const ser = api.float_sto(std::nullopt, std::nullopt, xfl, 0, 8); + BEAST_EXPECT(ser.has_value()); + auto const parsed = api.float_sto_set(ser.value()); + BEAST_EXPECT(parsed.has_value()); + BEAST_EXPECT( + api.float_compare(parsed.value(), xfl, compare_mode::EQUAL) + .value() == 1); + } + + void + test_float_sum(FeatureBitset features) + { + testcase("Test float_sum"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const one = api.float_one(); + auto const neg_one = api.float_negate(api.float_one()); + + // 1 + 1 = 2 + ASSERT_FLOAT_EQUAL( + api, 6090866696204910592LL, api.float_sum(one, one).value()); + + // 1 - 1 = 0 + ASSERT_FLOAT_EQUAL(api, 0, api.float_sum(one, neg_one).value()); + // 45678 + 0.345678 = 45678.345678 + ASSERT_FLOAT_EQUAL( + api, + 6165492124810638528LL, + api.float_sum(6165492090242838528LL, 6074309077695428608LL) + .value()); + // -151864512641 + 100000000000000000 = 99999848135487359 + ASSERT_FLOAT_EQUAL( + api, + 6387097057170171072LL, + api.float_sum(1676857706508234512LL, 6396111470866104320LL) + .value()); + + // auto generated random sums + // clang-format off + std::vector> tests = { + {7607324992379065667 /* 5.248821377668419e+84 */, 95785354843184473 /* -5.713362295774553e-77 */, 7607324992379065667 /* 5.248821377668419e+84 */}, + {1011203427860697296 /* -2.397111329706192e-26 */, 7715811566197737722 /* 5.64900413944857e+90 */, 7715811566197737722 /* 5.64900413944857e+90 */}, + {6507979072644559603 /* 4.781210721563379e+23 */, 422214339164556094 /* -7.883173446470462e-59 */, 6507979072644559603 /* 4.781210721563379e+23 */}, + {129493221419941559 /* -3.392431853567671e-75 */, 6742079437952459317 /* 4.694395406197301e+36 */, 6742079437952459317 /* 4.694395406197301e+36 */}, + {5172806703808250354 /* 2.674331586920946e-51 */, 3070396690523275533 /* -7.948943911338253e+88 */, 3070396690523275533 /* -7.948943911338253e+88 */}, + {2440992231195047997 /* -9.048432414980156e+53 */, 4937813945440933271 /* 1.868753842869655e-64 */, 2440992231195047996 /* -9.048432414980156e+53 */}, + {7351918685453062372 /* 2.0440935844129e+70 */, 6489541496844182832 /* 4.358033430668592e+22 */, 7351918685453062372 /* 2.0440935844129e+70 */}, + {4960621423606196948 /* 6.661833498651348e-63 */, 6036716382996689576 /* 0.001892882320224936 */, 6036716382996689576 /* 0.001892882320224936 */}, + {1342689232407435206 /* -9.62374270576839e-8 */, 5629833007898276923 /* 9.340672939897915e-26 */, 1342689232407435206 /* -9.62374270576839e-8 */}, + {7557687707019793516 /* 9.65473154684222e+81 */, 528084028396448719 /* -5.666471621471183e-53 */, 7557687707019793516 /* 9.65473154684222e+81 */}, + {130151633377050812 /* -4.050843810676924e-75 */, 2525286695563827336 /* -3.270904236349576e+58 */, 2525286695563827336 /* -3.270904236349576e+58 */}, + {5051914485221832639 /* 7.88290256687712e-58 */, 7518727241611221951 /* 6.723063157234623e+79 */, 7518727241611221951 /* 6.723063157234623e+79 */}, + {3014788764095798870 /* -6.384213012307542e+85 */, 7425019819707800346 /* 3.087633801222938e+74 */, 3014788764095767995 /* -6.384213012276667e+85 */}, + {4918950856932792129 /* 1.020063844210497e-65 */, 7173510242188034581 /* 3.779635414204949e+60 */, 7173510242188034581 /* 3.779635414204949e+60 */}, + {20028000442705357 /* -2.013601933223373e-81 */, 95248745393457140 /* -5.17675284604722e-77 */, 95248946753650462 /* -5.176954206240542e-77 */}, + {5516870225060928024 /* 4.46428115944092e-32 */, 7357202055584617194 /* 7.327463715967722e+70 */, 7357202055584617194 /* 7.327463715967722e+70 */}, + {2326103538819088036 /* -2.2461310959121e+47 */, 1749360946246242122 /* -1964290826489674 */, 2326103538819088036 /* -2.2461310959121e+47 */}, + {1738010758208819410 /* -862850129854894.6 */, 2224610859005732191 /* -8.83984233944816e+41 */, 2224610859005732192 /* -8.83984233944816e+41 */}, + {4869534730307487904 /* 5.647132747352224e-68 */, 2166841923565712115 /* -5.114102427874035e+38 */, 2166841923565712115 /* -5.114102427874035e+38 */}, + {1054339559322014937 /* -9.504445772059864e-24 */, 1389511416678371338 /* -0.0000240273144825857 */, 1389511416678371338 /* -0.0000240273144825857 */}, + }; + // clang-format on + + for (auto const& test : tests) + { + ASSERT_FLOAT_EQUAL( + api, + api.float_sum(std::get<0>(test), std::get<1>(test)).value(), + std::get<2>(test)); + } + } + + void + test_hook_account(FeatureBitset features) + { + testcase("Test hook_account"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + StubHookContext stubCtx{}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + + BEAST_EXPECT(api.hook_account() == alice.id()); + } + + void + test_hook_again(FeatureBitset features) + { + testcase("Test hook_again"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // Already requested + StubHookContext stubCtx{}; + stubCtx.result.executeAgainAsWeak = true; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + BEAST_EXPECT(hookCtx.result.executeAgainAsWeak); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.hook_again().error() == ALREADY_SET); + } + { + // Strong hook requests weak re-exec + StubHookContext stubCtx{}; + stubCtx.result.isStrong = true; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(!hookCtx.result.executeAgainAsWeak); + auto const result = api.hook_again(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.executeAgainAsWeak); + } + + { + // Not strong -> prerequisite not met + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + BEAST_EXPECT(!hookCtx.result.executeAgainAsWeak); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.hook_again().error() == PREREQUISITE_NOT_MET); + } + } + + void + test_hook_hash(FeatureBitset features) + { + testcase("Test hook_hash"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + + env(hook( + alice, + {{ + hso(genesis::AcceptHook), + hso(genesis::MintTestHook), + }}, + 0), + fee(XRP(100))); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + uint256 const expectedHash{2}; + StubHookContext stubCtx{ + .result = {.hookHash = expectedHash}, + }; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + + { + // current hook hash + auto const result = api.hook_hash(-1); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expectedHash); + } + + { + // Does not exist + auto const result = api.hook_hash(2); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + + { + // Success index = 0 + auto const wasm = genesis::AcceptHook; + auto const hash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + auto const result = api.hook_hash(0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == hash); + } + + { + // Success index = 1 + auto const wasm = genesis::MintTestHook; + auto const hash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + auto const result = api.hook_hash(1); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == hash); + } + } + + void + test_hook_param(FeatureBitset features) + { + testcase("Test hook_param"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // TOO_SMALL / TOO_BIG + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.hook_param({}).error() == TOO_SMALL); + BEAST_EXPECT(api.hook_param(Bytes(33, 1)).error() == TOO_BIG); + } + + // { + // // Hook params map + // Bytes const name{'k'}; + // Bytes const value{1, 2, 3}; + // StubHookContext stubCtx{ + // .result = { + // .hookParams = {{{name, value}}}, + // }}; + // auto hookCtx = + // makeStubHookContext(applyCtx, alice.id(), alice.id(), + // stubCtx); + // hook::HookAPI api(hookCtx); + // auto const result = api.hook_param(name); + // BEAST_EXPECT(result.has_value()); + // BEAST_EXPECT(result.value() == value); + // } + + { + // Override deletion wins + Bytes const name{'d'}; + StubHookContext stubCtx{}; + stubCtx.result.hookParamOverrides = { + {stubCtx.result.hookHash, {{name, Bytes{}}}}}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.hook_param(name).error() == DOESNT_EXIST); + } + + { + // Override takes precedence + Bytes const name{'o'}; + Bytes const overrideValue{9}; + StubHookContext stubCtx{}; + stubCtx.result.hookParams = + std::map{{name, Bytes{1}}}; // base value + stubCtx.result.hookParamOverrides = { + {stubCtx.result.hookHash, {{name, overrideValue}}}}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.hook_param(name); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == overrideValue); + } + } + + void + test_hook_param_set(FeatureBitset features) + { + testcase("Test hook_param_set"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hash = uint256{7}; + + { + // TOO_SMALL + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT( + api.hook_param_set(hash, {}, Bytes{1}).error() == TOO_SMALL); + } + + { + // TOO_BIG key/value and TOO_MANY_PARAMS + StubHookContext stubCtx{}; + stubCtx.result.overrideCount = hook_api::max_params; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT( + api.hook_param_set( + hash, + Bytes(hook::maxHookParameterKeySize() + 1, 1), + Bytes{}) + .error() == TOO_BIG); + BEAST_EXPECT( + api.hook_param_set( + hash, + Bytes{1}, + Bytes(hook::maxHookParameterValueSize() + 1, 1)) + .error() == TOO_BIG); + BEAST_EXPECT( + api.hook_param_set(hash, Bytes{1}, Bytes{1}).error() == + TOO_MANY_PARAMS); + } + + { + // SUCCESS and override stored + Bytes const name{'x'}; + Bytes const value{5, 6}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.hook_param_set(hash, name, value); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == value.size()); + BEAST_EXPECT(hookCtx.result.overrideCount == 1); + BEAST_EXPECT( + hookCtx.result.hookParamOverrides[hash].at(name) == value); + } + } + + void + test_hook_pos(FeatureBitset features) + { + testcase("Test hook_pos"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + StubHookContext stubCtx{}; + stubCtx.result.hookChainPosition = 3; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + + BEAST_EXPECT(api.hook_pos() == 3); + } + + void + test_hook_skip(FeatureBitset features) + { + testcase("Test hook_skip"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + env(hook(alice, {{hso(genesis::AcceptHook)}}, 0), fee(XRP(1))); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + { + .result = {.hookSkips = {uint256{123}, uint256{456}}}, + }); + auto& api = hookCtx.api(); + + { + // INVALID_ARGUMENT + auto const result = api.hook_skip(uint256{0}, 2); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + { + // DOESNT_EXIST + auto const result = api.hook_skip(uint256{1}, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + + { + // with Delete flag (1) + auto result = api.hook_skip(uint256{0}, 1); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 2); + + result = api.hook_skip(uint256{123}, 1); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 1); + } + + { + // already skipped + auto result = api.hook_skip(uint256{456}, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 1); + } + + { + // doesn't found + auto result = api.hook_skip(uint256{123}, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 1); + } + + { + // success + auto const wasm = genesis::AcceptHook; + auto const hash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + auto result = api.hook_skip(hash, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + BEAST_EXPECT(hookCtx.result.hookSkips.size() == 2); + BEAST_EXPECT( + hookCtx.result.hookSkips.find(hash) != + hookCtx.result.hookSkips.end()); + } + } + + void + test_ledger_keylet(FeatureBitset features) + { + testcase("Test ledger_keylet"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + env(cron::set(alice), cron::startTime(1000), fee(XRP(1))); + env.close(); + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + { + // does not match + auto const result = api.ledger_keylet( + keylet::account(alice), keylet::ownerDir(alice)); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOES_NOT_MATCH); + } + { + // does not exist + auto const result = + api.ledger_keylet(keylet::cron(100), keylet::cron(101)); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + + // Success case + { + auto const lo = keylet::cron(1000); + auto const hi = keylet::cron(1001); + auto const result = api.ledger_keylet(lo, hi); + BEAST_EXPECT(result.has_value()); + auto const expected = keylet::cron(1000, alice); + BEAST_EXPECT(result.value().type == expected.type); + BEAST_EXPECT(result.value().key == expected.key); + } + } + + void + test_ledger_last_hash(FeatureBitset features) + { + testcase("Test ledger_last_hash"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + BEAST_EXPECT(api.ledger_last_hash() == ov.info().parentHash); + } + + void + test_ledger_last_time(FeatureBitset features) + { + testcase("Test ledger_last_time"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + auto const expected = + ov.info().parentCloseTime.time_since_epoch().count(); + BEAST_EXPECT(api.ledger_last_time() == expected); + } + + void + test_ledger_nonce(FeatureBitset features) + { + testcase("Test ledger_nonce"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // TOO_MANY_NONCES + StubHookContext stubCtx{ + .ledger_nonce_counter = + static_cast(hook_api::max_nonce + 1)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.ledger_nonce(); + BEAST_EXPECT(result.error() == TOO_MANY_NONCES); + } + + { + // SUCCESS + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const info = hookCtx.applyCtx.view().info(); + auto const expected = ripple::sha512Half( + ripple::HashPrefix::hookNonce, + info.seq, + info.parentCloseTime.time_since_epoch().count(), + info.parentHash, + hookCtx.applyCtx.tx.getTransactionID(), + hookCtx.ledger_nonce_counter, + hookCtx.result.account); + auto const result = api.ledger_nonce(); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expected); + BEAST_EXPECT(hookCtx.ledger_nonce_counter == 1); + } + } + + void + test_ledger_seq(FeatureBitset features) + { + testcase("Test ledger_seq"); + + using namespace jtx; + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + BEAST_EXPECT(api.ledger_seq() == ov.info().seq); + } + + void + test_meta_slot(FeatureBitset features) + { + testcase("Test meta_slot"); + + using namespace jtx; + using namespace hook_api; + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + + env(noop(alice)); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + {.result = {.provisionalMeta = env.meta()}}); + auto& api = hookCtx.api(); + + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + // prerequisite not met + auto const result = api.meta_slot(0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // invalid argument + auto const result = api.meta_slot(hook_api::max_slots + 1); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + { + auto hookCtx = makeStubHookContext( + applyCtx, + alice.id(), + alice.id(), + {.result = {.provisionalMeta = env.meta()}}); + for (uint32_t i = 1; i <= hook_api::max_slots; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + // no free slots + auto& api = hookCtx.api(); + auto const result = api.meta_slot(0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == NO_FREE_SLOTS); + } + + { + auto& api = hookCtx.api(); + auto const result = api.meta_slot(0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == 1); + + BEAST_EXPECT(*hookCtx.slot[1].entry == *env.meta()); + } + } + + void + test_xpop_slot(FeatureBitset features) + { + testcase("Test xpop_slot"); + + using namespace jtx; + using namespace hook_api; + auto const alice = Account{"alice"}; + + Env env{*this, features}; + + STTx invokeTx = STTx(ttIMPORT, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + { + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + // invalid transaction type + auto const result = api.xpop_slot(0, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET); + } + + { + // invalid slot number + auto const result1 = api.xpop_slot(hook_api::max_slots + 1, 0); + BEAST_EXPECT(!result1.has_value()); + BEAST_EXPECT(result1.error() == INVALID_ARGUMENT); + + auto const result2 = api.xpop_slot(0, hook_api::max_slots + 1); + BEAST_EXPECT(!result2.has_value()); + BEAST_EXPECT(result2.error() == INVALID_ARGUMENT); + } + + { + // no free slots + for (uint32_t i = 1; i <= hook_api::max_slots - 1; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + auto& api = hookCtx.api(); + auto const result = api.xpop_slot(0, 0); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == NO_FREE_SLOTS); + } + + { + // same slot number for both + auto const result = api.xpop_slot(1, 1); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + // TODO: test INVALID_TXN + + { + // Success + auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed); + std::string xpopStr = Json::FastWriter().write(xpopJson); + STTx invokeTx = STTx(ttIMPORT, [&](STObject& obj) { + obj.setFieldVL(sfBlob, *strUnHex(strHex(xpopStr))); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.xpop_slot(0, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value().first == 1); + BEAST_EXPECT(result.value().second == 2); + Serializer stx, smeta; + hookCtx.slot[1].entry->add(stx); + hookCtx.slot[2].entry->add(smeta); + stx.getData(); + smeta.getData(); + + std::string blob = strHex(stx.getData()); + std::string meta = strHex(smeta.getData()); + BEAST_EXPECT(xpopJson["transaction"]["blob"] == blob); + BEAST_EXPECT(xpopJson["transaction"]["meta"] == meta); + } + } + + void + test_otxn_burden(FeatureBitset features) + { + testcase("Test otxn_burden"); + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx baseTx = STTx(ttINVOKE, [&](STObject& obj) {}); + + { + // Cached burden + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + StubHookContext stubCtx{.burden = 7}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_burden() == 7); + } + + { + // No sfEmitDetails + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_burden() == 1); + } + + { + // sfEmitDetails without sfEmitBurden + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_burden() == 1); + } + + { + // sfEmitBurden with high bit set is masked + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + auto& details = obj.peekFieldObject(sfEmitDetails); + details.setFieldU64( + sfEmitBurden, (static_cast(1) << 63) | 25); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const burden = api.otxn_burden(); + BEAST_EXPECT(burden == 25); + BEAST_EXPECT(api.otxn_burden() == burden); + } + } + + void + test_otxn_generation(FeatureBitset features) + { + testcase("Test otxn_generation"); + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx baseTx = STTx(ttINVOKE, [&](STObject& obj) {}); + + { + // Cached generation + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + StubHookContext stubCtx{.generation = 9}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_generation() == 9); + } + + { + // No sfEmitDetails + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, baseTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_generation() == 0); + } + + { + // sfEmitDetails without sfEmitGeneration + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_generation() == 0); + } + + { + // sfEmitGeneration present + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + obj.peekFieldObject(sfEmitDetails) + .setFieldU32(sfEmitGeneration, 4); + }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_generation() == 4); + BEAST_EXPECT(api.otxn_generation() == 4); + } + } + + void + test_otxn_field(FeatureBitset features) + { + testcase("Test otxn_field"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx tx = + STTx(ttINVOKE, [&](STObject& obj) { obj[sfAccount] = alice.id(); }); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // TODO: INVALID_FIELD now returns DOESNT_EXIST + // BEAST_EXPECT(api.otxn_field(0).error() == INVALID_FIELD); + BEAST_EXPECT( + api.otxn_field(sfDestination.getCode()).error() == DOESNT_EXIST); + auto const result = api.otxn_field(sfAccount.getCode()); + BEAST_EXPECT(result.has_value()); + auto const* acct = dynamic_cast(result.value()); + BEAST_EXPECT(acct != nullptr); + BEAST_EXPECT(acct->value() == alice.id()); + } + + void + test_otxn_id(FeatureBitset features) + { + testcase("Test otxn_id"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + + { + STTx tx = STTx(ttINVOKE, [&](STObject& obj) {}); + auto const txID = tx.getTransactionID(); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + // Originating transaction ID + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.otxn_id(0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == txID); + } + + { + STTx tx = STTx(ttEMIT_FAILURE, [&](STObject& obj) { + obj.setFieldH256(sfTransactionHash, uint256{5}); + }); + STTx emitFailedTx = STTx(ttINVOKE, [&](STObject& obj) {}); + auto const txID = tx.getTransactionID(); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + // Emit failure transaction ID + StubHookContext stubCtx{.emitFailure = emitFailedTx}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT( + api.otxn_id(0).value() == tx.getFieldH256(sfTransactionHash)); + // flags bypass emitFailure + BEAST_EXPECT(api.otxn_id(1).value() == txID); + } + } + + void + test_otxn_slot(FeatureBitset features) + { + testcase("Test otxn_slot"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx tx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + + { + // Invalid slot argument + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + auto const result = api.otxn_slot(hook_api::max_slots + 1); + BEAST_EXPECT(result.error() == INVALID_ARGUMENT); + } + + { + // No free slots + StubHookContext stubCtx{}; + for (uint32_t i = 1; i <= hook_api::max_slots; ++i) + stubCtx.slot[i] = hook::SlotEntry{}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + for (uint32_t i = 1; i <= hook_api::max_slots; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + BEAST_EXPECT(stubCtx.slot.size() == hook_api::max_slots); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_slot(0).error() == NO_FREE_SLOTS); + } + + { + // SUCCESS allocate new slot (slot = 0) + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + for (uint32_t i = 1; i <= 111; ++i) + hookCtx.slot[i] = hook::SlotEntry{}; + BEAST_EXPECT(!hookCtx.slot.contains(112)); + auto& api = hookCtx.api(); + auto const result = api.otxn_slot(0); + BEAST_EXPECT(result.has_value()); + auto const newSlot = result.value(); + BEAST_EXPECT(newSlot == 112); + BEAST_EXPECT(hookCtx.slot.contains(112)); + BEAST_EXPECT(hookCtx.slot[112].entry != nullptr); + // TODO: test slot content + } + + { + // SUCCESS allocate new slot (slot != 0) + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + BEAST_EXPECT(!hookCtx.slot.contains(200)); + auto& api = hookCtx.api(); + auto const result = api.otxn_slot(200); + BEAST_EXPECT(result.has_value()); + auto const newSlot = result.value(); + BEAST_EXPECT(newSlot == 200); + BEAST_EXPECT(hookCtx.slot.contains(200)); + BEAST_EXPECT(hookCtx.slot[newSlot].entry != nullptr); + // TODO: test slot content + } + } + + void + test_otxn_type(FeatureBitset features) + { + testcase("Test otxn_type"); + + using namespace jtx; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx tx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + + { + STObject failure(sfHook); + failure.setFieldU16(sfTransactionType, ttACCOUNT_SET); + StubHookContext stubCtx{.emitFailure = failure}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_type() == ttACCOUNT_SET); + } + + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.otxn_type() == ttINVOKE); + } + } + + void + test_otxn_param(FeatureBitset features) + { + testcase("Test otxn_param"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + + // Build tx with hook parameters + STTx tx = STTx(ttINVOKE, [&](STObject& obj) { + STArray params{sfHookParameters, 2}; + { + STObject p{sfHookParameter}; + p.setFieldVL(sfHookParameterName, Bytes{'a'}); + p.setFieldVL(sfHookParameterValue, Bytes{1, 2}); + params.emplace_back(std::move(p)); + } + { + STObject p{sfHookParameter}; + p.setFieldVL(sfHookParameterName, Bytes{'b'}); + // missing value to test DOESNT_EXIST + params.emplace_back(std::move(p)); + } + { + STObject p{sfHookParameter}; + p.setFieldVL(sfHookParameterName, Bytes{'c'}); + // empty value to test DOESNT_EXIST + p.setFieldVL(sfHookParameterValue, Bytes{}); + params.emplace_back(std::move(p)); + } + obj.setFieldArray(sfHookParameters, params); + }); + + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, tx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + BEAST_EXPECT(api.otxn_param({}).error() == TOO_SMALL); + BEAST_EXPECT(api.otxn_param(Bytes(33, 1)).error() == TOO_BIG); + auto const result = api.otxn_param(Bytes{'a'}); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == Bytes({1, 2})); + BEAST_EXPECT(api.otxn_param(Bytes{'b'}).error() == DOESNT_EXIST); + BEAST_EXPECT(api.otxn_param(Bytes{'c'}).error() == DOESNT_EXIST); + BEAST_EXPECT(api.otxn_param(Bytes{'d'}).error() == DOESNT_EXIST); + } + + void + test_slot(FeatureBitset features) + { + testcase("Test slot"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot(1).error() == DOESNT_EXIST); + } + + // Present slot pointing to an STAmount field + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(1)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = hook::SlotEntry{.storage = storage, .entry = 0}; + stubCtx.slot[1].entry = &(*stubCtx.slot[1].storage); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.slot(1); + BEAST_EXPECT(result.has_value()); + } + } + + void + test_slot_clear(FeatureBitset features) + { + testcase("Test slot_clear"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_clear(1).error() == DOESNT_EXIST); + } + + // Clear existing slot and push to free queue + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[2] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_clear(2).has_value()); + BEAST_EXPECT(hookCtx.slot.find(2) == hookCtx.slot.end()); + BEAST_EXPECT(!hookCtx.slot_free.empty()); + } + } + + void + test_slot_count(FeatureBitset features) + { + testcase("Test slot_count"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Nonexistent + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_count(1).error() == DOESNT_EXIST); + } + + // Not an array + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_count(1).error() == NOT_AN_ARRAY); + } + + // Array count + { + STArray arr{sfGeneric}; + arr.emplace_back(STObject(sfGeneric)); + arr.emplace_back(STObject(sfGeneric)); + auto storage = std::make_shared(arr); + StubHookContext stubCtx{}; + stubCtx.slot[3] = { + .storage = + std::reinterpret_pointer_cast(storage), + .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_count(3).value() == 2); + } + } + + void + test_slot_float(FeatureBitset features) + { + testcase("Test slot_float"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_float(1).error() == DOESNT_EXIST); + } + + // Not an amount + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_float(1).error() == NOT_AN_AMOUNT); + } + + // Native amount + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(10)); + auto storage = std::make_shared(obj); + auto* amtPtr = &storage->getFieldAmount(sfAmount); + StubHookContext stubCtx{}; + stubCtx.slot[2] = {.storage = storage, .entry = amtPtr}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.slot_float(2); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() != 0); + } + } + + void + test_slot_set(FeatureBitset features) + { + testcase("Test slot_set"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // Invalid argument (wrong size) + BEAST_EXPECT( + api.slot_set(Bytes{1, 2, 3}, 0).error() == INVALID_ARGUMENT); + // Invalid argument (slot_no beyond max) + BEAST_EXPECT( + api.slot_set(Bytes(32, 0), hook_api::max_slots + 1).error() == + INVALID_ARGUMENT); + } + + void + test_slot_size(FeatureBitset features) + { + testcase("Test slot_size"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_size(1).error() == DOESNT_EXIST); + } + + // Size of STObject + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(1)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_size(1).value() > 0); + } + } + + void + test_slot_subarray(FeatureBitset features) + { + testcase("Test slot_subarray"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing parent + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_subarray(1, 0, 0).error() == DOESNT_EXIST); + } + + // Valid subarray extraction + { + STArray arr{sfGeneric}; + arr.emplace_back(STObject(sfGeneric)); + arr.emplace_back(STObject(sfGeneric)); + auto storage = std::make_shared(arr); + StubHookContext stubCtx{}; + stubCtx.slot[1] = { + .storage = + std::reinterpret_pointer_cast(storage), + .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.slot_subarray(1, 1, 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() != 0); + BEAST_EXPECT(hookCtx.slot[result.value()].entry != nullptr); + } + } + + void + test_slot_subfield(FeatureBitset features) + { + testcase("Test slot_subfield"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing parent + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT( + api.slot_subfield(1, sfAccount.getCode(), 0).error() == + DOESNT_EXIST); + } + + // No free slots + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + for (int i = 0; i < hook_api::max_slots; i++) + hookCtx.slot[i] = hook::SlotEntry{}; + auto& api = hookCtx.api(); + BEAST_EXPECT( + api.slot_subfield(1, sfAccount.getCode(), 0).error() == + NO_FREE_SLOTS); + BEAST_EXPECT( + api.slot_subfield( + 1, sfAccount.getCode(), hook_api::max_slots + 1) + .error() == INVALID_ARGUMENT); + } + + // Invalid field + { + STObject obj(sfGeneric); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + BEAST_EXPECT( + api.slot_subfield(1, ((99U << 16U) + 99U), 0).error() == + INVALID_FIELD); + BEAST_EXPECT( + api.slot_subfield(1, sfAccount.getCode(), 0).error() == + DOESNT_EXIST); + } + + // Valid field + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(1)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.slot_subfield(1, sfAmount.getCode(), 0); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() != 0); + BEAST_EXPECT(hookCtx.slot[result.value()].entry != nullptr); + // test the amount bytes + } + } + + void + test_slot_type(FeatureBitset features) + { + testcase("Test slot_type"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + // Missing slot + { + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + BEAST_EXPECT(api.slot_type(1, 0).error() == DOESNT_EXIST); + } + + // Generic object + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(5)); + auto storage = std::make_shared(obj); + StubHookContext stubCtx{}; + stubCtx.slot[1] = {.storage = storage, .entry = &(*storage)}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.slot_type(1, 0); + BEAST_EXPECT(result.has_value()); + } + + // Amount with flag 1 + { + STObject obj(sfGeneric); + obj.setFieldAmount(sfAmount, drops(7)); + auto storage = std::make_shared(obj); + auto* amtPtr = &storage->getFieldAmount(sfAmount); + StubHookContext stubCtx{}; + stubCtx.slot[2] = {.storage = storage, .entry = amtPtr}; + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx); + auto& api = hookCtx.api(); + auto const result = api.slot_type(2, 1); + BEAST_EXPECT(result.has_value()); + } + } + + void + test_state(FeatureBitset features) + { + testcase("Test state"); + + // state() is same as state_foreign with hook's namespace and account + // tested in test_state_foreign + BEAST_EXPECT(true); + } + + void + test_state_foreign(FeatureBitset features) + { + testcase("Test state_foreign"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + // Note: Full state API testing requires proper hook execution + // environment which is tested in SetHook_test.cpp integration tests. + // Here we test cache behavior and error conditions that can be + // simulated without actual ledger state access. + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(10000), alice); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + uint256 const testKey{1}; + uint256 const testNs{2}; + + { + // Test cache read path: when data is already in stateMap cache + // Use external stateMap to avoid dangling reference + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate the stateMap cache directly + Bytes expectedData{0x01, 0x02, 0x03}; + stateMap[alice.id()] = { + 100, // availableForReserves + 1, // namespaceCount + 1, // hookStateScale + {{testNs, {{testKey, {false, expectedData}}}}}}; + + auto& api = hookCtx.api(); + auto const result = api.state_foreign(testKey, testNs, alice.id()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == expectedData); + } + + { + // Test cache miss path: key not in cache + // Since view().peek() will be called for cache miss and will not + // find state in ledger, it should return DOESNT_EXIST + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate with different key + uint256 const otherKey{99}; + Bytes data{0xFF}; + stateMap[alice.id()] = { + 100, 1, 1, {{testNs, {{otherKey, {false, data}}}}}}; + + auto& api = hookCtx.api(); + // testKey is not in cache, so it will try to read from ledger + // which should return DOESNT_EXIST since there's no actual state + auto const result = api.state_foreign(testKey, testNs, alice.id()); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == DOESNT_EXIST); + } + } + + void + test_state_foreign_set_max(FeatureBitset features) + { + testcase("Test state_foreign_set max"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + env.fund(XRP(100000), alice); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + { + // TOO_MANY_STATE_MODIFICATIONS + // This check only applies when creating a NEW key, not updating + // existing + uint256 const testKey{1}; + uint256 const existingKey{2}; + uint256 const testNs{3}; + + // Use external stateMap to avoid dangling reference + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate stateMap with a DIFFERENT key (existingKey, not + // testKey) so that testKey will be a new entry + stateMap[alice.id()] = { + 100, // availableForReserves + 1, // namespaceCount + 1, // hookStateScale + {{testNs, {{existingKey, {false, Bytes{}}}}}}}; + + // Set modified_entry_count to max + stateMap.modified_entry_count = max_state_modifications; + + auto& api = hookCtx.api(); + Bytes data{0x01}; + // This should fail because testKey is new and we're at max + // modifications + auto const result = + api.state_foreign_set(testKey, testNs, alice.id(), data); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == TOO_MANY_STATE_MODIFICATIONS); + } + + { + // Updating existing key also fails at max modifications + // (TOO_MANY_STATE_MODIFICATIONS limits total modifications, not + // just new keys) + uint256 const testKey{1}; + uint256 const testNs{2}; + + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate with the SAME key we'll update + stateMap[alice.id()] = { + 100, 1, 1, {{testNs, {{testKey, {false, Bytes{0xFF}}}}}}}; + + stateMap.modified_entry_count = max_state_modifications; + + auto& api = hookCtx.api(); + Bytes data{0x01}; + // This should also fail because TOO_MANY_STATE_MODIFICATIONS + // applies to all modifications, not just new keys + auto const result = + api.state_foreign_set(testKey, testNs, alice.id(), data); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == TOO_MANY_STATE_MODIFICATIONS); + } + } + + void + test_state_foreign_set(FeatureBitset features) + { + testcase("Test state_foreign_set"); + + using namespace jtx; + using namespace hook_api; + using namespace hook; + + // Note: Full state_foreign_set testing requires proper hook execution + // environment which is tested in SetHook_test.cpp integration tests. + // Here we test what can be verified through cache manipulation. + + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + Env env{*this, features}; + env.fund(XRP(100000), alice, bob); + env.close(); + + // Compute hook hash for the accept hook + auto const wasm = genesis::AcceptHook; + auto const hookHash = + ripple::sha512Half_s(ripple::Slice(wasm.data(), wasm.size())); + + // Set up a hook on bob (no grants) + env(hook(bob, {{hso(genesis::AcceptHook)}}, 0), fee(XRP(1))); + env.close(); + + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + + uint256 const testKey{1}; + uint256 const testNs{2}; + Bytes testData{0x01, 0x02, 0x03}; + + { + // Local account modification with pre-populated stateMap + // (equivalent to state_set test) + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate stateMap to avoid reserve calculation issues + stateMap[alice.id()] = { + 100, // availableForReserves + 1, // namespaceCount + 1, // hookStateScale + {}}; + + auto& api = hookCtx.api(); + + auto const result = + api.state_foreign_set(testKey, testNs, alice.id(), testData); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT(result.value() == testData.size()); + auto const stateMapAcc = + std::get<3>(hookCtx.result.stateMap[alice.id()]); + auto const stateMapNs = stateMapAcc.find(testNs); + BEAST_EXPECT(stateMapNs != stateMapAcc.end()); + auto const stateMapKey = stateMapNs->second.find(testKey); + BEAST_EXPECT(stateMapKey != stateMapNs->second.end()); + BEAST_EXPECT(std::get<0>(stateMapKey->second) == true); + } + + { + // Delete operation (empty data) + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate stateMap with existing data + stateMap[alice.id()] = { + 100, // availableForReserves + 1, // namespaceCount + 1, // hookStateScale + {{testNs, {{testKey, {false, testData}}}}}}; + + auto& api = hookCtx.api(); + + // Delete (empty data) + Bytes emptyData{}; + auto deleteResult = + api.state_foreign_set(testKey, testNs, alice.id(), emptyData); + BEAST_EXPECT(deleteResult.has_value()); + BEAST_EXPECT(deleteResult.value() == 0); + auto const stateMapAcc = + std::get<3>(hookCtx.result.stateMap[alice.id()]); + auto const stateMapNs = stateMapAcc.find(testNs); + BEAST_EXPECT(stateMapNs != stateMapAcc.end()); + auto const stateMapKey = stateMapNs->second.find(testKey); + BEAST_EXPECT(stateMapKey != stateMapNs->second.end()); + BEAST_EXPECT(std::get<0>(stateMapKey->second) == true); + BEAST_EXPECT(std::get<1>(stateMapKey->second) == emptyData); + } + + { + // PREVIOUS_FAILURE_PREVENTS_RETRY + StubHookContext stubCtx{}; + stubCtx.result.foreignStateSetDisabled = true; + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), stubCtx, stateMap); + auto& api = hookCtx.api(); + + auto const result = + api.state_foreign_set(testKey, testNs, bob.id(), testData); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == PREVIOUS_FAILURE_PREVENTS_RETRY); + } + + { + // NOT_AUTHORIZED: foreign state set without grant + StubHookContext stubCtx{ + .result = {.hookHash = hookHash}, + }; + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), stubCtx, stateMap); + auto& api = hookCtx.api(); + + auto const result = + api.state_foreign_set(testKey, testNs, bob.id(), testData); + BEAST_EXPECT(!result.has_value()); + BEAST_EXPECT(result.error() == NOT_AUTHORIZED); + // After NOT_AUTHORIZED, foreignStateSetDisabled should be set + BEAST_EXPECT(hookCtx.result.foreignStateSetDisabled); + } + + { + // Cache hit: modify same state twice without re-checking grants + HookStateMap stateMap; + auto hookCtx = makeStubHookContext( + applyCtx, alice.id(), alice.id(), {}, stateMap); + + // Pre-populate stateMap + stateMap[alice.id()] = { + 100, // availableForReserves + 1, // namespaceCount + 1, // hookStateScale + {}}; + + auto& api = hookCtx.api(); + + // First modification + auto result1 = + api.state_foreign_set(testKey, testNs, alice.id(), testData); + BEAST_EXPECT(result1.has_value()); + + // Second modification (should hit cache) + Bytes newData{0x04, 0x05}; + auto result2 = + api.state_foreign_set(testKey, testNs, alice.id(), newData); + BEAST_EXPECT(result2.has_value()); + BEAST_EXPECT(result2.value() == newData.size()); + } + } + + void + test_state_set(FeatureBitset features) + { + testcase("Test state_set"); + + // state_set() is same as state_foreign_set with hook's namespace and + // account tested in test_state_foreign_set + BEAST_EXPECT(true); + } + + void + test_sto_emplace(FeatureBitset features) + { + testcase("Test sto_emplace/sto_erase"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + { + // Invalid argument (wrong source_object size) + auto const small = Bytes(1, 0); + auto const large = Bytes(1024 * 16 + 1, 0); + + auto const small_result = + api.sto_emplace(small, std::nullopt, sfAccount.getCode()); + BEAST_EXPECT(!small_result.has_value()); + BEAST_EXPECT(small_result.error() == TOO_SMALL); + + auto const large_result = + api.sto_emplace(large, std::nullopt, sfAccount.getCode()); + BEAST_EXPECT(!large_result.has_value()); + BEAST_EXPECT(large_result.error() == TOO_BIG); + } + + { + // Invalid argument (wrong field_object size) + auto const source = Bytes(16, 0); + auto const small = Bytes(1, 0); + auto const large = Bytes(4096 + 1, 0); + + auto const small_result = + api.sto_emplace(source, small, sfAccount.getCode()); + BEAST_EXPECT(!small_result.has_value()); + BEAST_EXPECT(small_result.error() == TOO_SMALL); + + auto const large_result = + api.sto_emplace(source, large, sfAccount.getCode()); + BEAST_EXPECT(!large_result.has_value()); + BEAST_EXPECT(large_result.error() == TOO_BIG); + } + + { + // TODO: test parse error + } + + { + // Success + auto const source_object = + *strUnHex("81140000000000000000000000000000000000000000"); + // Account: "" + auto const field_object = *strUnHex("2400000000"); + // Sequence: 0 + + auto const result = api.sto_emplace( + source_object, field_object, sfSequence.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + BEAST_EXPECT( + result.value() == + *strUnHex( + "240000000081140000000000000000000000000000000000000000")); + } + + auto const _source_object = + *strUnHex("81140000000000000000000000000000000000000000"); + { + // use UINT16 + auto source_object = _source_object; + auto field_object = *strUnHex("10100001"); + // Version: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfVersion.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfVersion.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // use UINT32 + auto source_object = _source_object; + auto field_object = *strUnHex("2400000001"); + // Sequence: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfSequence.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfSequence.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // use UINT64 + auto source_object = _source_object; + auto field_object = *strUnHex("360000000000000001"); + // ExchangeRate: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfExchangeRate.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfExchangeRate.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // use UINT128 + auto source_object = _source_object; + auto field_object = *strUnHex("4100000000000000000000000000000000"); + // EmailHash: 1 + + auto const result = api.sto_emplace( + source_object, field_object, sfEmailHash.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfEmailHash.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // use UINT256 + auto source_object = _source_object; + auto field_object = *strUnHex( + "5E000000000000000000000000000000000000000000000000000000000000" + "0000"); + // ObjectID: + // "0000000000000000000000000000000000000000000000000000000000000000" + + auto const result = api.sto_emplace( + source_object, field_object, sfObjectID.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), source_object.begin(), source_object.end()); + BEAST_EXPECT(result.value() == field_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfObjectID.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // use AMOUNT + auto source_object = _source_object; + + auto nativeamount = *strUnHex( + "61999999999999999999999999999999999999999999999999999999999999" + "999999999999999999999999999999999999"); + auto iouamount = *strUnHex("614999999999999999"); + + for (auto field_object : {nativeamount, iouamount}) + { + auto const result = api.sto_emplace( + source_object, field_object, sfAmount.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + field_object.insert( + field_object.end(), + source_object.begin(), + source_object.end()); + BEAST_EXPECT(result.value() == field_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfAmount.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT( + erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + } + + { + // OBJECT + auto source_object = _source_object; + auto field_object = *strUnHex("E05B614000000000000064E1"); + // {"AmountEntry": {"Amount": "100"}} + + auto const result = api.sto_emplace( + source_object, field_object, sfAmountEntry.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfAmountEntry.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // ARRAY + auto source_object = _source_object; + auto field_object = *strUnHex("F9EA7D04DEADBEEFE1F1"); + // {"Memos": [{"Memo":{ "MemoData": "DEADBEEF" }}]} + + auto const result = + api.sto_emplace(source_object, field_object, sfMemos.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfMemos.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // UINT8 + auto source_object = _source_object; + auto field_object = *strUnHex("00101001"); + // {"TickSize": 1} + + auto const result = api.sto_emplace( + source_object, field_object, sfTickSize.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfTickSize.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // UINT160 + auto source_object = _source_object; + auto field_object = + *strUnHex("01110000000000000000000000005553440000000000"); + // {"TakerPaysCurrency": "0000000000000000000000005553440000000000"} + + auto const result = api.sto_emplace( + source_object, field_object, sfTakerPaysCurrency.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfTakerPaysCurrency.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // PATHSET + auto source_object = _source_object; + auto field_object = *strUnHex( + "0112300000000000000000000000005553440000000000054F6F784A58F9EF" + "B0A9EB90B83464F9D166461900"); + // {"Paths": [[{ "currency": "USD", "issuer": + // "rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc" }]]} + + auto const result = + api.sto_emplace(source_object, field_object, sfPaths.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfPaths.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // VECTOR256 + auto source_object = _source_object; + auto field_object = *strUnHex( + "03132042426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97" + "ABF044EE"); + // {"Amendments":["42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE"]} + + auto const result = api.sto_emplace( + source_object, field_object, sfAmendments.getCode()); + BEAST_EXPECT(result.has_value()); + BEAST_EXPECT( + result.value().size() == + source_object.size() + field_object.size()); + source_object.insert( + source_object.end(), field_object.begin(), field_object.end()); + BEAST_EXPECT(result.value() == source_object); + + auto const erase_result = api.sto_emplace( + result.value(), std::nullopt, sfAmendments.getCode()); + BEAST_EXPECT(erase_result.has_value()); + BEAST_EXPECT(erase_result.value().size() == _source_object.size()); + BEAST_EXPECT(erase_result.value() == _source_object); + } + + { + // UINT96 + } + + { + // UINT384 + } + + { + // UINT512 + } + } + + void + test_sto_subarray(FeatureBitset features) + { + testcase("Test sto_subarray"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + { + // Invalid data size + BEAST_EXPECT(api.sto_subarray(Bytes{}, 0).error() == TOO_SMALL); + BEAST_EXPECT(api.sto_subarray(Bytes{0x00}, 0).error() == TOO_SMALL); + } + + { + // Invalid: wrapped but size = 0 ([]) + // { Memos: [] } + BEAST_EXPECT( + api.sto_subarray(Bytes{0xF9, 0xF1}, 0).error() == PARSE_ERROR); + // { Amounts: [] } + BEAST_EXPECT( + api.sto_subarray(Bytes{0xF0, 0x5C, 0xF1}, 0).error() == + PARSE_ERROR); + } + + { + // doesn't found + // { Memos: [{Memo: {MemoData: "BEEF"}}] } + auto const memos = *strUnHex("F9EA7D02BEEFE1F1"); + BEAST_EXPECT(api.sto_subarray(memos, 2).error() == DOESNT_EXIST); + // { Amounts: [{AmountEntry: {Amount: "100"}}] } + auto const amounts = *strUnHex("F05CE05B614000000000000064E1F1"); + BEAST_EXPECT(api.sto_subarray(amounts, 2).error() == DOESNT_EXIST); + } + + { + // success + // { Memos: [{Memo: {MemoData: "BEEF"}}] } + auto const memos = *strUnHex("F9EA7D02BEEFE1F1"); + BEAST_EXPECT( + api.sto_subarray(memos, 0).value() == std::make_pair(1u, 6u)); + // { Amounts: [{AmountEntry: {Amount: "100"}}] } + auto const amounts = *strUnHex("F05CE05B614000000000000064E1F1"); + BEAST_EXPECT( + api.sto_subarray(amounts, 0).value() == + std::make_pair(2u, 12u)); + } + } + + void + test_sto_subfield(FeatureBitset features) + { + testcase("Test sto_subfield"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + { + // Invalid data size + BEAST_EXPECT(api.sto_subfield(Bytes{}, 0).error() == TOO_SMALL); + BEAST_EXPECT(api.sto_subfield(Bytes{0x00}, 0).error() == TOO_SMALL); + } + + { + // Invalid data + BEAST_EXPECT( + api.sto_subfield(Bytes{0xFF, 0xFF, 0xFF, 0xFF}, 0).error() == + PARSE_ERROR); + } + + { + // doesn't found + // { Memo: {MemoData: "BEEF"} } + auto const memos = *strUnHex("EA7D02BEEFE1"); + BEAST_EXPECT( + api.sto_subfield(memos, sfMemoData.getCode()).error() == + DOESNT_EXIST); + // { AmountEntry: {Amount: "100"} } + auto const amounts = *strUnHex("E05B614000000000000064E1"); + BEAST_EXPECT( + api.sto_subfield(amounts, sfAmount.getCode()).error() == + DOESNT_EXIST); + } + + { + // success + // { Memo: {MemoData: "BEEF"} } + auto const memos = *strUnHex("EA7D02BEEFE1"); + BEAST_EXPECT( + api.sto_subfield(memos, sfMemo.getCode()).value() == + std::make_pair(1u, 4u)); + // { AmountEntry: {Amount: "100"} } + auto const amounts = *strUnHex("E05B614000000000000064E1"); + BEAST_EXPECT( + api.sto_subfield(amounts, sfAmountEntry.getCode()).value() == + std::make_pair(2u, 9u)); + } + } + + void + test_sto_validate(FeatureBitset features) + { + testcase("Test sto_validate"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL); + BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL); + + // { Memo: {MemoData: "BEEF"} } + auto const memos = *strUnHex("EA7D02BEEFE1"); + // { AmountEntry: {Amount: "100"} } + auto const amounts = *strUnHex("E05B614000000000000064E1"); + // { Memo: {MemoData: "BEEF"} } + auto const memo = *strUnHex("EA7D02BEEFE1"); + // { AmountEntry: {Amount: "100"} } + auto const amountEntry = *strUnHex("E05B614000000000000064E1"); + + BEAST_EXPECT(api.sto_validate(memos).value() == true); + BEAST_EXPECT(api.sto_validate(amounts).value() == true); + BEAST_EXPECT(api.sto_validate(memo).value() == true); + BEAST_EXPECT(api.sto_validate(amountEntry).value() == true); + + // Invalid data + BEAST_EXPECT( + api.sto_validate(Bytes{0xFF, 0xFF, 0xFF, 0xFF}).value() == false); + + Bytes const i_memos(&memos[0], &memos[memos.size() - 1]); + Bytes const i_amounts(&amounts[0], &amounts[amounts.size() - 1]); + Bytes const i_memo(&memo[0], &memo[memo.size() - 1]); + Bytes const i_amountEntry( + &amountEntry[0], &amountEntry[amountEntry.size() - 1]); + BEAST_EXPECT(api.sto_validate(i_memos).value() == false); + BEAST_EXPECT(api.sto_validate(i_amounts).value() == false); + BEAST_EXPECT(api.sto_validate(i_memo).value() == false); + BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false); + + Bytes const i2_memos(&memos[1], &memos[memos.size()]); + Bytes const i2_amounts(&amounts[1], &amounts[amounts.size()]); + Bytes const i2_memo(&memo[1], &memo[memo.size()]); + Bytes const i2_amountEntry( + &amountEntry[1], &amountEntry[amountEntry.size()]); + BEAST_EXPECT(api.sto_validate(i_memos).value() == false); + BEAST_EXPECT(api.sto_validate(i_amounts).value() == false); + BEAST_EXPECT(api.sto_validate(i_memo).value() == false); + BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false); + } + + void + test_trace(FeatureBitset features) + { + testcase("Test trace"); + + BEAST_EXPECT(true); + } + + void + test_trace_float(FeatureBitset features) + { + testcase("Test trace_float"); + + BEAST_EXPECT(true); + } + + void + test_trace_num(FeatureBitset features) + { + testcase("Test trace_num"); + + BEAST_EXPECT(true); + } + + void + test_util_accid(FeatureBitset features) + { + testcase("Test util_accid"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // Invalid base58 string + BEAST_EXPECT(api.util_accid("invalid").error() == INVALID_ARGUMENT); + + // Valid r-address round-trip via util_raddr + auto accid = api.util_accid(alice.human()); + BEAST_EXPECT(accid.has_value()); + auto aliceid = alice.id(); + BEAST_EXPECT(accid.value() == Bytes(aliceid.begin(), aliceid.end())); + } + + void + test_util_keylet(FeatureBitset features) + { + testcase("Test util_keylet"); + + // TODO + BEAST_EXPECT(true); + } + + void + test_util_raddr(FeatureBitset features) + { + testcase("Test util_raddr"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // Wrong size + BEAST_EXPECT(api.util_raddr(Bytes(10, 0)).error() == INVALID_ARGUMENT); + + // Valid accountID + auto aliceid = alice.id(); + auto id = Bytes(aliceid.begin(), aliceid.end()); + auto addr = api.util_raddr(id); + BEAST_EXPECT(addr.has_value()); + BEAST_EXPECT(addr.value() == alice.human()); + } + + void + test_util_sha512h(FeatureBitset features) + { + testcase("Test util_sha512h"); + + using namespace jtx; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + std::string msg{"hello"}; + auto hash = api.util_sha512h(Slice(msg.data(), msg.size())); + auto expected = ripple::sha512Half(Slice(msg.data(), msg.size())); + BEAST_EXPECT(hash == expected); + } + + void + test_util_verify(FeatureBitset features) + { + testcase("Test util_verify"); + + using namespace jtx; + using namespace hook; + using namespace hook_api; + + auto const alice = Account{"alice"}; + Env env{*this, features}; + STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {}); + OpenView ov{*env.current()}; + ApplyContext applyCtx = createApplyContext(env, ov, invokeTx); + auto hookCtx = + makeStubHookContext(applyCtx, alice.id(), alice.id(), {}); + auto& api = hookCtx.api(); + + // Generate keypair and signature + KeyType type = KeyType::secp256k1; + auto kp = generateKeyPair(type, generateSeed("util_verify_test")); + Serializer s; + s.add32(42); + auto const msg = s.slice(); + auto sig = ripple::sign(kp.first, kp.second, msg); + + // Invalid key size + BEAST_EXPECT( + api.util_verify(msg, sig, kp.first.slice().substr(0, 32)).error() == + INVALID_KEY); + + // Invalid data size + BEAST_EXPECT( + api.util_verify(Slice{}, sig, kp.first.slice()).error() == + TOO_SMALL); + + // Invalid sig size + BEAST_EXPECT( + api.util_verify(msg, Slice(sig.data(), 29), kp.first.slice()) + .error() == TOO_SMALL); + + // Invalid sig type + auto const& invalidKey = kp.first.slice().substr(1, 34); + BEAST_EXPECT( + api.util_verify(msg, sig, invalidKey).error() == INVALID_KEY); + return; + + // Success + auto const ok = api.util_verify(msg, sig, kp.first.slice()); + BEAST_EXPECT(ok.has_value()); + BEAST_EXPECT(ok.value()); + } + + void + testWithFeatures(FeatureBitset features) + { + using namespace test::jtx; + test_accept(features); + test_rollback(features); + testGuards(features); + + test_emit(features); + test_etxn_burden(features); + test_etxn_generation(features); + test_otxn_burden(features); + test_otxn_generation(features); + test_etxn_details(features); + test_etxn_fee_base(features - fixHookAPI20251128); + test_etxn_fee_base(features); + test_etxn_nonce(features); + test_etxn_reserve(features); + test_fee_base(features); + + test_otxn_field(features); + + test_ledger_keylet(features); + + test_float_compare(features); + test_float_divide(features); + test_float_int(features); + test_float_invert(features); + test_float_log(features); + test_float_mantissa(features); + test_float_mulratio(features); + test_float_multiply(features); + test_float_negate(features); + test_float_one(features); + test_float_root(features); + test_float_set(features); + test_float_sign(features); + test_float_sto(features); + test_float_sto_set(features); + test_float_sum(features); + + test_hook_account(features); + test_hook_again(features); + test_hook_hash(features); + test_hook_param(features); + test_hook_param_set(features); + test_hook_pos(features); + test_hook_skip(features); + + test_ledger_last_hash(features); + test_ledger_last_time(features); + test_ledger_nonce(features); + test_ledger_seq(features); + + test_meta_slot(features); + test_xpop_slot(features); + + test_otxn_id(features); + test_otxn_slot(features); + test_otxn_type(features); + test_otxn_param(features); + + test_slot(features); + test_slot_clear(features); + test_slot_count(features); + test_slot_float(features); + test_slot_set(features); + test_slot_size(features); + test_slot_subarray(features); + test_slot_subfield(features); + test_slot_type(features); + + test_state(features); + test_state_foreign(features); + test_state_foreign_set(features); + test_state_foreign_set_max(features); + test_state_set(features); + + test_sto_emplace(features); + // test_sto_erase(features); // tested in test_sto_emplace + test_sto_subarray(features); + test_sto_subfield(features); + test_sto_validate(features); + + test_trace(features); + test_trace_float(features); + test_trace_num(features); + + test_util_accid(features); + test_util_keylet(features); + test_util_raddr(features); + test_util_sha512h(features); + test_util_verify(features); + } + +public: + void + run() override + { + using namespace test::jtx; + testWithFeatures(supported_amendments()); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(HookAPI, app, ripple, 2); +} // namespace test +} // namespace ripple +#undef M diff --git a/src/test/app/SetHook_test.cpp b/src/test/app/SetHook_test.cpp index 81cb317df..c12d7055e 100644 --- a/src/test/app/SetHook_test.cpp +++ b/src/test/app/SetHook_test.cpp @@ -2579,6 +2579,7 @@ public: auto const alice = Account{"alice"}; auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); @@ -3094,6 +3095,7 @@ public: extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code); extern int64_t etxn_details (uint32_t, uint32_t); extern int64_t etxn_reserve(uint32_t); + extern int64_t hook_hash (uint32_t, uint32_t, int32_t); #define TOO_SMALL -4 #define OUT_OF_BOUNDS -1 #define PREREQUISITE_NOT_MET -9 @@ -3116,6 +3118,45 @@ public: etxn_reserve(1); ASSERT(etxn_details((uint32_t)det, 116) == 116); + uint8_t expected1[49] = { + 0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xB8U, 0x05U, 0xD6U, + 0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, 0xC0U, 0x20U, 0x47U, + 0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, 0xFAU, 0x0DU, 0xE3U, + 0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, 0xEBU, 0x68U, + }; + // 0x5CU + // EmitNonce 32bytes + uint8_t expected_emit_nonce[32] = { + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU + }; + // 0x5DU, + // EmitHookHash + uint8_t expected_hook_hash[32] = { + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, + }; + // 0xE1U + + // current hook hash + ASSERT(hook_hash((uint32_t)expected_hook_hash, 32, -1) == 32); + + for (int i = 0; GUARD(49), i < sizeof(expected1); ++i) + ASSERT(det[i] == expected1[i]); + ASSERT(det[49] == 0x5CU); + // TODO: need to test this + // for (int i = 0; GUARD(32), i < sizeof(expected_emit_nonce); ++i) + // ASSERT(det[50 + i] == expected_emit_nonce[i]); + ASSERT(det[82] == 0x5DU); + for (int i = 0; GUARD(32), i < sizeof(expected_hook_hash); ++i) + ASSERT(det[83 + i] == expected_hook_hash[i]); + ASSERT(det[115] == 0xE1); + return accept(0,0,0); } )[test.hook]"]; @@ -3280,6 +3321,7 @@ public: } ASSERT(etxn_nonce((uint32_t)nonce, 116) == TOO_MANY_NONCES); + ASSERT(etxn_nonce((uint32_t)nonce, 31) == TOO_MANY_NONCES); return accept(0,0,0); } diff --git a/src/test/app/SetHook_wasm.h b/src/test/app/SetHook_wasm.h index 73294c9fb..4918bfb45 100644 --- a/src/test/app/SetHook_wasm.h +++ b/src/test/app/SetHook_wasm.h @@ -1168,6 +1168,7 @@ std::map> wasm = { extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code); extern int64_t etxn_details (uint32_t, uint32_t); extern int64_t etxn_reserve(uint32_t); + extern int64_t hook_hash (uint32_t, uint32_t, int32_t); #define TOO_SMALL -4 #define OUT_OF_BOUNDS -1 #define PREREQUISITE_NOT_MET -9 @@ -1190,78 +1191,178 @@ std::map> wasm = { etxn_reserve(1); ASSERT(etxn_details((uint32_t)det, 116) == 116); + uint8_t expected1[49] = { + 0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xB8U, 0x05U, 0xD6U, + 0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, 0xC0U, 0x20U, 0x47U, + 0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, 0xFAU, 0x0DU, 0xE3U, + 0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, 0xEBU, 0x68U, + }; + // 0x5CU + // EmitNonce 32bytes + uint8_t expected_emit_nonce[32] = { + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU + }; + // 0x5DU, + // EmitHookHash + uint8_t expected_hook_hash[32] = { + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, + 0xFFU, 0xFFU, + }; + // 0xE1U + + // current hook hash + ASSERT(hook_hash((uint32_t)expected_hook_hash, 32, -1) == 32); + + for (int i = 0; GUARD(49), i < sizeof(expected1); ++i) + ASSERT(det[i] == expected1[i]); + ASSERT(det[49] == 0x5CU); + // TODO: need to test this + // for (int i = 0; GUARD(32), i < sizeof(expected_emit_nonce); ++i) + // ASSERT(det[50 + i] == expected_emit_nonce[i]); + ASSERT(det[82] == 0x5DU); + for (int i = 0; GUARD(32), i < sizeof(expected_hook_hash); ++i) + ASSERT(det[83 + i] == expected_hook_hash[i]); + ASSERT(det[115] == 0xE1); + return accept(0,0,0); } )[test.hook]", { - 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x19U, - 0x04U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x02U, 0x7FU, + 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x20U, + 0x05U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, - 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0x4CU, 0x05U, 0x03U, 0x65U, - 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, - 0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, - 0x61U, 0x69U, 0x6CU, 0x73U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, - 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU, 0x00U, - 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, - 0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x03U, - 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, - 0x74U, 0x00U, 0x02U, 0x03U, 0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, - 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0xF0U, 0x89U, - 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xE5U, 0x09U, 0x0BU, 0x7FU, 0x00U, - 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xF0U, 0x89U, 0x04U, - 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x07U, 0x08U, 0x01U, - 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x05U, 0x0AU, 0x84U, 0x82U, - 0x00U, 0x01U, 0x80U, 0x82U, 0x00U, 0x02U, 0x01U, 0x7FU, 0x01U, 0x7EU, - 0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x80U, 0x01U, 0x6BU, - 0x22U, 0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, - 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, - 0x40U, 0x41U, 0xC0U, 0x84U, 0x3DU, 0x41U, 0xF4U, 0x00U, 0x10U, 0x81U, + 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7FU, + 0x01U, 0x7EU, 0x02U, 0x5CU, 0x06U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, + 0x5FU, 0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x65U, + 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, + 0x73U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, + 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU, 0x00U, 0x02U, 0x03U, 0x65U, + 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x72U, 0x65U, + 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, + 0x76U, 0x09U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U, + 0x68U, 0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, + 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U, 0x03U, 0x02U, 0x01U, 0x03U, + 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, + 0x41U, 0xD0U, 0x8BU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xC6U, 0x0BU, + 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, + 0xD0U, 0x8BU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, + 0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x06U, + 0x0AU, 0xF4U, 0x84U, 0x00U, 0x01U, 0xF0U, 0x84U, 0x00U, 0x02U, 0x03U, + 0x7FU, 0x01U, 0x7EU, 0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, + 0xA0U, 0x01U, 0x6BU, 0x22U, 0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, + 0x00U, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, + 0x00U, 0x1AU, 0x02U, 0x40U, 0x41U, 0xC0U, 0x84U, 0x3DU, 0x41U, 0xF4U, + 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x7FU, 0x51U, + 0x0DU, 0x00U, 0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x2CU, + 0x42U, 0x16U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, + 0x02U, 0x40U, 0x41U, 0x00U, 0x41U, 0xC0U, 0x84U, 0x3DU, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x7FU, 0x51U, 0x0DU, 0x00U, 0x41U, - 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x2CU, 0x42U, 0x15U, 0x10U, - 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x41U, - 0x00U, 0x41U, 0xC0U, 0x84U, 0x3DU, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, - 0x00U, 0x42U, 0x7FU, 0x51U, 0x0DU, 0x00U, 0x41U, 0xACU, 0x88U, 0x80U, - 0x80U, 0x00U, 0x41U, 0x2AU, 0x42U, 0x16U, 0x10U, 0x82U, 0x80U, 0x80U, - 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xF3U, - 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x7CU, 0x51U, - 0x0DU, 0x00U, 0x41U, 0xD6U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x2EU, - 0x42U, 0x18U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, - 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xF4U, 0x00U, 0x10U, 0x81U, 0x80U, - 0x80U, 0x80U, 0x00U, 0x42U, 0x77U, 0x51U, 0x0DU, 0x00U, 0x41U, 0x84U, - 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x39U, 0x42U, 0x1AU, 0x10U, 0x82U, - 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x01U, 0x10U, 0x83U, - 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, - 0xF4U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0xF4U, - 0x00U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xBDU, 0x89U, 0x80U, 0x80U, 0x00U, - 0x41U, 0x28U, 0x42U, 0x1DU, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, - 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x84U, - 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x02U, 0x20U, 0x01U, 0x41U, 0x80U, - 0x01U, 0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x02U, - 0x0BU, 0x0BU, 0xEDU, 0x01U, 0x01U, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, - 0xE5U, 0x01U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, - 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, - 0x30U, 0x30U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, - 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, 0x5FU, 0x4FU, 0x46U, 0x5FU, 0x42U, - 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, - 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, 0x30U, - 0x2CU, 0x20U, 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x29U, - 0x20U, 0x3DU, 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, 0x5FU, 0x4FU, 0x46U, - 0x5FU, 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, 0x74U, - 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, - 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, - 0x29U, 0x64U, 0x65U, 0x74U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x35U, 0x29U, - 0x20U, 0x3DU, 0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x53U, 0x4DU, - 0x41U, 0x4CU, 0x4CU, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, - 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, 0x28U, 0x75U, 0x69U, - 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x64U, 0x65U, 0x74U, - 0x2CU, 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, - 0x50U, 0x52U, 0x45U, 0x52U, 0x45U, 0x51U, 0x55U, 0x49U, 0x53U, 0x49U, - 0x54U, 0x45U, 0x5FU, 0x4EU, 0x4FU, 0x54U, 0x5FU, 0x4DU, 0x45U, 0x54U, - 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, - 0x69U, 0x6CU, 0x73U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, - 0x32U, 0x5FU, 0x74U, 0x29U, 0x64U, 0x65U, 0x74U, 0x2CU, 0x20U, 0x31U, - 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x31U, 0x36U, - 0x00U, + 0xACU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x2AU, 0x42U, 0x17U, 0x10U, + 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, + 0x01U, 0x41U, 0x20U, 0x6AU, 0x41U, 0xF3U, 0x00U, 0x10U, 0x81U, 0x80U, + 0x80U, 0x80U, 0x00U, 0x42U, 0x7CU, 0x51U, 0x0DU, 0x00U, 0x41U, 0xD6U, + 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x2EU, 0x42U, 0x19U, 0x10U, 0x82U, + 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, + 0x41U, 0x20U, 0x6AU, 0x41U, 0xF4U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, + 0x80U, 0x00U, 0x42U, 0x77U, 0x51U, 0x0DU, 0x00U, 0x41U, 0x84U, 0x89U, + 0x80U, 0x80U, 0x00U, 0x41U, 0x39U, 0x42U, 0x1BU, 0x10U, 0x82U, 0x80U, + 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x01U, 0x10U, 0x83U, 0x80U, + 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, + 0x6AU, 0x41U, 0xF4U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, + 0x42U, 0xF4U, 0x00U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xBDU, 0x89U, 0x80U, + 0x80U, 0x00U, 0x41U, 0x28U, 0x42U, 0x1EU, 0x10U, 0x82U, 0x80U, 0x80U, + 0x80U, 0x00U, 0x1AU, 0x0BU, 0x20U, 0x01U, 0x41U, 0x18U, 0x6AU, 0x42U, + 0x7FU, 0x37U, 0x03U, 0x00U, 0x20U, 0x01U, 0x41U, 0x10U, 0x6AU, 0x42U, + 0x7FU, 0x37U, 0x03U, 0x00U, 0x20U, 0x01U, 0x42U, 0x7FU, 0x37U, 0x03U, + 0x08U, 0x20U, 0x01U, 0x42U, 0x7FU, 0x37U, 0x03U, 0x00U, 0x02U, 0x40U, + 0x20U, 0x01U, 0x41U, 0x20U, 0x41U, 0x7FU, 0x10U, 0x84U, 0x80U, 0x80U, + 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xA1U, 0x8AU, + 0x80U, 0x80U, 0x00U, 0x41U, 0x36U, 0x42U, 0x3AU, 0x10U, 0x82U, 0x80U, + 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0xBCU, 0x80U, 0x80U, 0x80U, + 0x78U, 0x41U, 0x32U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, + 0x41U, 0x00U, 0x21U, 0x02U, 0x03U, 0x40U, 0x41U, 0xBCU, 0x80U, 0x80U, + 0x80U, 0x78U, 0x41U, 0x32U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, + 0x1AU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU, 0x20U, 0x02U, + 0x6AU, 0x2DU, 0x00U, 0x00U, 0x20U, 0x02U, 0x41U, 0xF0U, 0x89U, 0x80U, + 0x80U, 0x00U, 0x6AU, 0x2DU, 0x00U, 0x00U, 0x46U, 0x0DU, 0x00U, 0x41U, + 0xD7U, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, 0x17U, 0x42U, 0x3DU, 0x10U, + 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x20U, 0x02U, 0x41U, + 0x01U, 0x6AU, 0x22U, 0x02U, 0x41U, 0x31U, 0x47U, 0x0DU, 0x00U, 0x0BU, + 0x02U, 0x40U, 0x20U, 0x01U, 0x2DU, 0x00U, 0x51U, 0x41U, 0xDCU, 0x00U, + 0x46U, 0x0DU, 0x00U, 0x41U, 0xEEU, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, + 0x11U, 0x42U, 0x3EU, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, + 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x2DU, 0x00U, 0x72U, 0x41U, 0xDDU, + 0x00U, 0x46U, 0x0DU, 0x00U, 0x41U, 0xFFU, 0x8AU, 0x80U, 0x80U, 0x00U, + 0x41U, 0x11U, 0x42U, 0xC2U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, + 0x00U, 0x1AU, 0x0BU, 0x41U, 0xC3U, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U, + 0x21U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, + 0x41U, 0xF3U, 0x00U, 0x6AU, 0x21U, 0x03U, 0x41U, 0x00U, 0x21U, 0x02U, + 0x03U, 0x40U, 0x41U, 0xC3U, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U, 0x21U, + 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, 0x20U, + 0x03U, 0x20U, 0x02U, 0x6AU, 0x2DU, 0x00U, 0x00U, 0x20U, 0x01U, 0x20U, + 0x02U, 0x6AU, 0x2DU, 0x00U, 0x00U, 0x46U, 0x0DU, 0x00U, 0x41U, 0x90U, + 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x25U, 0x42U, 0xC4U, 0x00U, 0x10U, + 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x20U, 0x02U, 0x41U, + 0x01U, 0x6AU, 0x22U, 0x02U, 0x41U, 0x20U, 0x47U, 0x0DU, 0x00U, 0x0BU, + 0x02U, 0x40U, 0x20U, 0x01U, 0x2DU, 0x00U, 0x93U, 0x01U, 0x41U, 0xE1U, + 0x01U, 0x46U, 0x0DU, 0x00U, 0x41U, 0xB5U, 0x8BU, 0x80U, 0x80U, 0x00U, + 0x41U, 0x11U, 0x42U, 0xC5U, 0x00U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, + 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, + 0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x04U, 0x20U, 0x01U, 0x41U, + 0xA0U, 0x01U, 0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, + 0x04U, 0x0BU, 0x0BU, 0xCEU, 0x03U, 0x01U, 0x00U, 0x41U, 0x80U, 0x08U, + 0x0BU, 0xC6U, 0x03U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, + 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, 0x31U, 0x30U, 0x30U, 0x30U, + 0x30U, 0x30U, 0x30U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, + 0x3DU, 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, 0x5FU, 0x4FU, 0x46U, 0x5FU, + 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, 0x74U, 0x78U, + 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, + 0x30U, 0x2CU, 0x20U, 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, + 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, 0x5FU, 0x4FU, + 0x46U, 0x5FU, 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, + 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, + 0x73U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, + 0x74U, 0x29U, 0x64U, 0x65U, 0x74U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x35U, + 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x53U, + 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, + 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, 0x28U, 0x75U, + 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x64U, 0x65U, + 0x74U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, 0x3DU, + 0x20U, 0x50U, 0x52U, 0x45U, 0x52U, 0x45U, 0x51U, 0x55U, 0x49U, 0x53U, + 0x49U, 0x54U, 0x45U, 0x5FU, 0x4EU, 0x4FU, 0x54U, 0x5FU, 0x4DU, 0x45U, + 0x54U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, + 0x61U, 0x69U, 0x6CU, 0x73U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, + 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x64U, 0x65U, 0x74U, 0x2CU, 0x20U, + 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x31U, + 0x36U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, + 0x3DU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, + 0xB8U, 0x05U, 0xD6U, 0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, + 0xC0U, 0x20U, 0x47U, 0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, + 0xFAU, 0x0DU, 0xE3U, 0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, + 0xEBU, 0x68U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U, + 0x68U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, + 0x74U, 0x29U, 0x65U, 0x78U, 0x70U, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U, + 0x5FU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U, 0x68U, + 0x2CU, 0x20U, 0x33U, 0x32U, 0x2CU, 0x20U, 0x2DU, 0x31U, 0x29U, 0x20U, + 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x64U, 0x65U, 0x74U, 0x5BU, + 0x69U, 0x5DU, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x65U, 0x78U, 0x70U, 0x65U, + 0x63U, 0x74U, 0x65U, 0x64U, 0x31U, 0x5BU, 0x69U, 0x5DU, 0x00U, 0x64U, + 0x65U, 0x74U, 0x5BU, 0x34U, 0x39U, 0x5DU, 0x20U, 0x3DU, 0x3DU, 0x20U, + 0x30U, 0x78U, 0x35U, 0x43U, 0x55U, 0x00U, 0x64U, 0x65U, 0x74U, 0x5BU, + 0x38U, 0x32U, 0x5DU, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x78U, 0x35U, + 0x44U, 0x55U, 0x00U, 0x64U, 0x65U, 0x74U, 0x5BU, 0x38U, 0x33U, 0x20U, + 0x2BU, 0x20U, 0x69U, 0x5DU, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x65U, 0x78U, + 0x70U, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U, 0x5FU, 0x68U, 0x6FU, 0x6FU, + 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U, 0x68U, 0x5BU, 0x69U, 0x5DU, 0x00U, + 0x64U, 0x65U, 0x74U, 0x5BU, 0x31U, 0x31U, 0x35U, 0x5DU, 0x20U, 0x3DU, + 0x3DU, 0x20U, 0x30U, 0x78U, 0x45U, 0x31U, 0x00U, }}, /* ==== WASM: 8 ==== */ @@ -1422,6 +1523,7 @@ std::map> wasm = { } ASSERT(etxn_nonce((uint32_t)nonce, 116) == TOO_MANY_NONCES); + ASSERT(etxn_nonce((uint32_t)nonce, 31) == TOO_MANY_NONCES); return accept(0,0,0); } @@ -1437,12 +1539,12 @@ std::map> wasm = { 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x02U, 0x03U, 0x02U, 0x01U, 0x03U, 0x05U, 0x03U, 0x01U, 0x00U, - 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0xA0U, 0x8AU, 0x04U, - 0x0BU, 0x7FU, 0x00U, 0x41U, 0x9EU, 0x0AU, 0x0BU, 0x7FU, 0x00U, 0x41U, - 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xA0U, 0x8AU, 0x04U, 0x0BU, + 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0xE0U, 0x8AU, 0x04U, + 0x0BU, 0x7FU, 0x00U, 0x41U, 0xD1U, 0x0AU, 0x0BU, 0x7FU, 0x00U, 0x41U, + 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xE0U, 0x8AU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, - 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x04U, 0x0AU, 0xE1U, 0x82U, 0x00U, - 0x01U, 0xDDU, 0x82U, 0x00U, 0x02U, 0x02U, 0x7FU, 0x01U, 0x7EU, 0x23U, + 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x04U, 0x0AU, 0x84U, 0x83U, 0x00U, + 0x01U, 0x80U, 0x83U, 0x00U, 0x02U, 0x02U, 0x7FU, 0x01U, 0x7EU, 0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0xC0U, 0x00U, 0x6BU, 0x22U, 0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U, @@ -1474,39 +1576,48 @@ std::map> wasm = { 0x20U, 0x01U, 0x41U, 0xF4U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x74U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xEAU, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x34U, 0x42U, 0x23U, 0x10U, 0x82U, 0x80U, 0x80U, - 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, - 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U, 0x03U, 0x20U, 0x01U, - 0x41U, 0xC0U, 0x00U, 0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, - 0x20U, 0x03U, 0x0BU, 0x0BU, 0xA6U, 0x02U, 0x01U, 0x00U, 0x41U, 0x80U, - 0x08U, 0x0BU, 0x9EU, 0x02U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x6EU, - 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, - 0x30U, 0x30U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, - 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, 0x5FU, 0x4FU, 0x46U, 0x5FU, 0x42U, - 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, - 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x30U, 0x2CU, 0x20U, - 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x29U, 0x20U, 0x3DU, - 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, 0x5FU, 0x4FU, 0x46U, 0x5FU, 0x42U, - 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, - 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x28U, 0x75U, 0x69U, - 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x6EU, 0x6FU, 0x6EU, - 0x63U, 0x65U, 0x2CU, 0x20U, 0x33U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, - 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, + 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x1FU, + 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x74U, 0x51U, 0x0DU, + 0x00U, 0x41U, 0x9EU, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, 0x33U, 0x42U, + 0x24U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, + 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, + 0x00U, 0x21U, 0x03U, 0x20U, 0x01U, 0x41U, 0xC0U, 0x00U, 0x6AU, 0x24U, + 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x03U, 0x0BU, 0x0BU, 0xD9U, + 0x02U, 0x01U, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0xD1U, 0x02U, 0x65U, + 0x74U, 0x78U, 0x6EU, 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, + 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x30U, 0x2CU, 0x20U, 0x31U, + 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, + 0x5FU, 0x4FU, 0x46U, 0x5FU, 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, + 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, + 0x65U, 0x28U, 0x30U, 0x2CU, 0x20U, 0x31U, 0x30U, 0x30U, 0x30U, 0x30U, + 0x30U, 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x4FU, 0x55U, 0x54U, + 0x5FU, 0x4FU, 0x46U, 0x5FU, 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, - 0x74U, 0x29U, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x20U, 0x2BU, 0x20U, - 0x28U, 0x28U, 0x69U, 0x20U, 0x25U, 0x20U, 0x32U, 0x29U, 0x20U, 0x2AU, - 0x20U, 0x33U, 0x32U, 0x29U, 0x2CU, 0x20U, 0x33U, 0x32U, 0x29U, 0x20U, - 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x21U, 0x28U, 0x2AU, 0x28U, - 0x6EU, 0x31U, 0x20U, 0x2BU, 0x20U, 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, - 0x20U, 0x2AU, 0x28U, 0x6EU, 0x32U, 0x20U, 0x2BU, 0x20U, 0x30U, 0x29U, - 0x20U, 0x26U, 0x26U, 0x20U, 0x2AU, 0x28U, 0x6EU, 0x31U, 0x20U, 0x2BU, - 0x20U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x2AU, 0x28U, 0x6EU, - 0x32U, 0x20U, 0x2BU, 0x20U, 0x31U, 0x29U, 0x29U, 0x00U, 0x65U, 0x74U, - 0x78U, 0x6EU, 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x28U, - 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x6EU, - 0x6FU, 0x6EU, 0x63U, 0x65U, 0x2CU, 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, - 0x20U, 0x3DU, 0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x4DU, 0x41U, - 0x4EU, 0x59U, 0x5FU, 0x4EU, 0x4FU, 0x4EU, 0x43U, 0x45U, 0x53U, 0x00U, + 0x74U, 0x29U, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x2CU, 0x20U, 0x33U, + 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, + 0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, + 0x5FU, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x28U, 0x75U, 0x69U, + 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x6EU, 0x6FU, 0x6EU, + 0x63U, 0x65U, 0x20U, 0x2BU, 0x20U, 0x28U, 0x28U, 0x69U, 0x20U, 0x25U, + 0x20U, 0x32U, 0x29U, 0x20U, 0x2AU, 0x20U, 0x33U, 0x32U, 0x29U, 0x2CU, + 0x20U, 0x33U, 0x32U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, + 0x00U, 0x21U, 0x28U, 0x2AU, 0x28U, 0x6EU, 0x31U, 0x20U, 0x2BU, 0x20U, + 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x2AU, 0x28U, 0x6EU, 0x32U, + 0x20U, 0x2BU, 0x20U, 0x30U, 0x29U, 0x20U, 0x26U, 0x26U, 0x20U, 0x2AU, + 0x28U, 0x6EU, 0x31U, 0x20U, 0x2BU, 0x20U, 0x31U, 0x29U, 0x20U, 0x3DU, + 0x3DU, 0x20U, 0x2AU, 0x28U, 0x6EU, 0x32U, 0x20U, 0x2BU, 0x20U, 0x31U, + 0x29U, 0x29U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x6EU, 0x6FU, + 0x6EU, 0x63U, 0x65U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, + 0x32U, 0x5FU, 0x74U, 0x29U, 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x2CU, + 0x20U, 0x31U, 0x31U, 0x36U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x54U, + 0x4FU, 0x4FU, 0x5FU, 0x4DU, 0x41U, 0x4EU, 0x59U, 0x5FU, 0x4EU, 0x4FU, + 0x4EU, 0x43U, 0x45U, 0x53U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, + 0x6EU, 0x6FU, 0x6EU, 0x63U, 0x65U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, + 0x74U, 0x33U, 0x32U, 0x5FU, 0x74U, 0x29U, 0x6EU, 0x6FU, 0x6EU, 0x63U, + 0x65U, 0x2CU, 0x20U, 0x33U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, + 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x4DU, 0x41U, 0x4EU, 0x59U, 0x5FU, 0x4EU, + 0x4FU, 0x4EU, 0x43U, 0x45U, 0x53U, 0x00U, }}, /* ==== WASM: 10 ==== */ diff --git a/src/test/jtx/hook.h b/src/test/jtx/hook.h index 6bad339cc..f9113dcaa 100644 --- a/src/test/jtx/hook.h +++ b/src/test/jtx/hook.h @@ -20,9 +20,13 @@ #ifndef RIPPLE_TEST_JTX_HOOK_H_INCLUDED #define RIPPLE_TEST_JTX_HOOK_H_INCLUDED +#include #include +#include +#include #include #include +#include namespace ripple { namespace test { @@ -43,6 +47,75 @@ hso(std::string const& wasmHex, void (*f)(Json::Value& jv) = 0); Json::Value hso_delete(void (*f)(Json::Value& jv) = 0); +struct StubHookResult +{ + ripple::uint256 const hookSetTxnID = ripple::uint256(); + ripple::uint256 const hookHash = ripple::uint256(); + ripple::uint256 const hookCanEmit = ripple::uint256(); + ripple::uint256 const hookNamespace = ripple::uint256(); + + std::queue> emittedTxn{}; + std::optional stateMap = std::nullopt; + uint16_t changedStateCount = 0; + std::map< + ripple::uint256, // hook hash + std::map< + std::vector, // hook param name + std::vector // hook param value + >> + hookParamOverrides = {}; + + std::optional, std::vector>> + hookParams = std::nullopt; + std::set hookSkips = {}; + hook_api::ExitType exitType = hook_api::ExitType::ROLLBACK; + std::string exitReason{""}; + int64_t exitCode{-1}; + uint64_t instructionCount{0}; + bool hasCallback = false; + bool isCallback = false; + bool isStrong = false; + uint32_t wasmParam = 0; + uint32_t overrideCount = 0; + uint8_t hookChainPosition = 0; + bool foreignStateSetDisabled = false; + bool executeAgainAsWeak = false; + std::shared_ptr provisionalMeta = nullptr; +}; + +struct StubHookContext +{ + std::map slot{}; + std::queue slot_free{}; + uint32_t slot_counter{0}; + uint16_t emit_nonce_counter{0}; + uint16_t ledger_nonce_counter{0}; + int64_t expected_etxn_count{-1}; + std::map nonce_used{}; + uint32_t generation = 0; + uint64_t burden = 0; + std::map guard_map{}; + StubHookResult result = {}; + std::optional emitFailure = std::nullopt; + const hook::HookExecutor* module = 0; +}; + +// Overload that takes external stateMap to avoid dangling reference +hook::HookContext +makeStubHookContext( + ripple::ApplyContext& applyCtx, + ripple::AccountID const& hookAccount, + ripple::AccountID const& otxnAccount, + StubHookContext const& stubHookContext, + hook::HookStateMap& stateMap); + +hook::HookContext +makeStubHookContext( + ripple::ApplyContext& applyCtx, + ripple::AccountID const& hookAccount, + ripple::AccountID const& otxnAccount, + StubHookContext const& stubHookContext); + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/hook.cpp b/src/test/jtx/impl/hook.cpp index 08067a2a2..91b6f6d0c 100644 --- a/src/test/jtx/impl/hook.cpp +++ b/src/test/jtx/impl/hook.cpp @@ -18,7 +18,9 @@ //============================================================================== #include +#include #include +#include #include #include #include @@ -102,6 +104,81 @@ hso(std::string const& wasmHex, void (*f)(Json::Value& jv)) return jv; } +// Helper function to create HookContext with external stateMap +hook::HookContext +makeStubHookContext( + ripple::ApplyContext& applyCtx, + ripple::AccountID const& hookAccount, + ripple::AccountID const& otxnAccount, + StubHookContext const& stubHookContext, + hook::HookStateMap& stateMap) +{ + auto& result = stubHookContext.result; + auto hookParams = result.hookParams.value_or( + std::map, std::vector>{}); + return hook::HookContext{ + .applyCtx = applyCtx, + .slot = stubHookContext.slot, + .slot_free = stubHookContext.slot_free, + .slot_counter = stubHookContext.slot_counter, + .emit_nonce_counter = stubHookContext.emit_nonce_counter, + .ledger_nonce_counter = stubHookContext.ledger_nonce_counter, + .expected_etxn_count = stubHookContext.expected_etxn_count, + .nonce_used = stubHookContext.nonce_used, + .generation = stubHookContext.generation, + .burden = stubHookContext.burden, + .guard_map = stubHookContext.guard_map, + .result = + { + .hookSetTxnID = result.hookSetTxnID, + .hookHash = result.hookHash, + .hookCanEmit = result.hookCanEmit, + .accountKeylet = keylet::account(hookAccount), + .hookKeylet = keylet::hook(hookAccount), + .account = hookAccount, + .otxnAccount = otxnAccount, + .hookNamespace = result.hookNamespace, + .emittedTxn = result.emittedTxn, + .stateMap = stateMap, + .changedStateCount = result.changedStateCount, + .hookParamOverrides = result.hookParamOverrides, + .hookParams = hookParams, + .hookSkips = result.hookSkips, + .exitType = result.exitType, + .exitReason = result.exitReason, + .exitCode = result.exitCode, + .instructionCount = result.instructionCount, + .hasCallback = result.hasCallback, + .isCallback = result.isCallback, + .isStrong = result.isStrong, + .wasmParam = result.wasmParam, + .overrideCount = result.overrideCount, + .hookChainPosition = result.hookChainPosition, + .foreignStateSetDisabled = result.foreignStateSetDisabled, + .executeAgainAsWeak = result.executeAgainAsWeak, + .provisionalMeta = result.provisionalMeta, + }, + .emitFailure = stubHookContext.emitFailure, + .module = nullptr}; +} + +// Original function - WARNING: stateMap reference may become dangling +// Only use when stateMap access is not needed after HookContext creation +hook::HookContext +makeStubHookContext( + ripple::ApplyContext& applyCtx, + ripple::AccountID const& hookAccount, + ripple::AccountID const& otxnAccount, + StubHookContext const& stubHookContext) +{ + // Use thread_local to keep stateMap alive + // Note: This is a workaround; each call resets the stateMap + thread_local hook::HookStateMap stateMap; + stateMap = stubHookContext.result.stateMap.value_or(hook::HookStateMap{}); + return makeStubHookContext( + applyCtx, hookAccount, otxnAccount, stubHookContext, stateMap); +} + } // namespace jtx } // namespace test } // namespace ripple From edafb1fed6cf054480e873f53cdadedbd30fb3df Mon Sep 17 00:00:00 2001 From: tequ Date: Tue, 17 Feb 2026 10:32:34 +0900 Subject: [PATCH 4/5] fix `Xahau Ledger` to `Xahau Network` (#651) --- README.md | 2 +- SECURITY.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0a7e17ddd..30efdb776 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ git-subtree. See those directories' README files for more details. - [Xrpl Documentation](https://xrpl.org) - [Xahau Documentation](https://xahau.network/) - [Hooks Technical Documentation](https://xrpl-hooks.readme.io/) -- **Explorers**: Explore the Xahau ledger using various explorers: +- **Explorers**: Explore the Xahau Network using various explorers: - [xahauexplorer.com](https://xahauexplorer.com) - [xahscan.com](https://xahscan.com) - [xahau.xrpl.org](https://xahau.xrpl.org) diff --git a/SECURITY.md b/SECURITY.md index 1bd1ff7e1..1d98e1ff9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -62,11 +62,11 @@ For these complaints or reports, please [contact our support team](mailto:bugs@x ### The following type of security problems are excluded 1. **In scope**. Only bugs in software under the scope of the program qualify. Currently, that means `xahaud` and `xahau-lib`. -2. **Relevant**. A security issue, posing a danger to user funds, privacy or the operation of the Xahau Ledger. +2. **Relevant**. A security issue, posing a danger to user funds, privacy or the operation of the Xahau Network. 3. **Original and previously unknown**. Bugs that are already known and discussed in public do not qualify. Previously reported bugs, even if publicly unknown, are not eligible. 4. **Specific**. We welcome general security advice or recommendations, but we cannot pay bounties for that. 5. **Fixable**. There has to be something we can do to permanently fix the problem. Note that bugs in other people’s software may still qualify in some cases. For example, if you find a bug in a library that we use which can compromise the security of software that is in scope and we can get it fixed, you may qualify for a bounty. -6. **Unused**. If you use the exploit to attack the Xahau Ledger, you do not qualify for a bounty. If you report a vulnerability used in an ongoing or past attack and there is specific, concrete evidence that suggests you are the attacker we reserve the right not to pay a bounty. +6. **Unused**. If you use the exploit to attack the Xahau Network, you do not qualify for a bounty. If you report a vulnerability used in an ongoing or past attack and there is specific, concrete evidence that suggests you are the attacker we reserve the right not to pay a bounty. Please note: Reports that are lacking any proof (such as screenshots or other data), detailed information or details on how to reproduce any unexpected result will be investigated but will not be eligible for any reward. From 309e517e7029f2e4d8c7589df642f376a6065ccc Mon Sep 17 00:00:00 2001 From: tequ Date: Tue, 17 Feb 2026 10:34:12 +0900 Subject: [PATCH 5/5] Add GitHub Actions workflow for Guard Checker Build (#658) --- .github/workflows/guard-checker-build.yml | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/guard-checker-build.yml diff --git a/.github/workflows/guard-checker-build.yml b/.github/workflows/guard-checker-build.yml new file mode 100644 index 000000000..e3e7f6cbc --- /dev/null +++ b/.github/workflows/guard-checker-build.yml @@ -0,0 +1,24 @@ +name: Guard Checker Build + +on: + push: + pull_request: + +jobs: + guard-checker-build: + strategy: + fail-fast: false + matrix: + include: + - run-on: ubuntu-latest + - run-on: macos-latest + runs-on: ${{ matrix.run-on }} + name: Guard Checker Build - ${{ matrix.run-on }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build Guard Checker + run: | + cd src/ripple/app/hook + make guard_checker