From a7900a7f36e984e2340fe2c1731952f32b187c87 Mon Sep 17 00:00:00 2001 From: Niq Dudfield Date: Mon, 16 Feb 2026 15:51:04 +0700 Subject: [PATCH] Merge dev (d20927237) into sync-2.4.0: HookAPI refactor (#681) * Hook API Refactor2: Amendment Guards (#621) * Hook API Refactor3: Consolidate the Hook API definitions from Enum.h and ApplyHook.h into a single file. (#622) * Hook API Refactoring / Unit Testing (#581) * Hook API Refactor2: Amendment Guards (#621) * Hook API Refactor3: Consolidate the Hook API definitions from Enum.h and ApplyHook.h into a single file. (#622) * Hook API Refactoring / Unit Testing (#581) * fix: update clang-format to v18 and fix include ordering - Update verify-generated-headers CI to use clang-format 18 (matching clang-format.yml) instead of stale v10 which can't parse .clang-format - Add .mise.toml for local clang-format 18 tooling - Fix include ordering in cherry-picked files per clang-format 18 * chore: update levelization results for HookAPI changes New loop: xrpl.hook <-> xrpld.app due to HookAPI.h including Transaction.h from xrpld.app. --------- Co-authored-by: tequ --- .../workflows/verify-generated-headers.yml | 16 +- .mise.toml | 2 + Builds/levelization/results/loops.txt | 3 + Builds/levelization/results/ordering.txt | 1 - hook/extern.h | 22 +- hook/generate_extern.sh | 137 +- include/xrpl/basics/Expected.h | 7 +- include/xrpl/hook/Enum.h | 161 +- include/xrpl/hook/Guard.h | 46 +- include/xrpl/hook/HookAPI.h | 346 ++ include/xrpl/hook/Macro.h | 82 +- include/xrpl/hook/guard_checker.cpp | 12 +- include/xrpl/hook/hook_api.macro | 369 ++ src/test/app/HookAPI_test.cpp | 4543 +++++++++++++++++ src/test/app/SetHook_test.cpp | 57 +- src/test/app/SetHook_wasm.h | 315 +- src/test/jtx/hook.h | 73 + src/test/jtx/impl/hook.cpp | 77 + src/xrpld/app/hook/applyHook.h | 476 +- src/xrpld/app/hook/detail/HookAPI.cpp | 3197 ++++++++++++ src/xrpld/app/hook/detail/applyHook.cpp | 2932 ++--------- src/xrpld/app/tx/detail/Change.cpp | 5 +- src/xrpld/app/tx/detail/SetHook.cpp | 4 +- 23 files changed, 9408 insertions(+), 3475 deletions(-) create mode 100644 .mise.toml create mode 100644 include/xrpl/hook/HookAPI.h create mode 100644 include/xrpl/hook/hook_api.macro create mode 100644 src/test/app/HookAPI_test.cpp create mode 100644 src/xrpld/app/hook/detail/HookAPI.cpp diff --git a/.github/workflows/verify-generated-headers.yml b/.github/workflows/verify-generated-headers.yml index 4120f7b7f..861d1d2a1 100644 --- a/.github/workflows/verify-generated-headers.yml +++ b/.github/workflows/verify-generated-headers.yml @@ -18,12 +18,26 @@ jobs: generator: bash ./hook/generate_sfcodes.sh - target: hook/tts.h generator: ./hook/generate_tts.sh - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + env: + CLANG_VERSION: 18 name: ${{ matrix.target }} steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Install clang-format + run: | + codename=$( lsb_release --codename --short ) + sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null < xrpl.hook + Loop: xrpl.protocol xrpld.app xrpld.app > xrpl.protocol diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index 0c1dffac7..26bf3240f 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -154,7 +154,6 @@ xrpld.app > xrpl.basics xrpld.app > xrpld.conditions xrpld.app > xrpld.consensus xrpld.app > xrpld.perflog -xrpld.app > xrpl.hook xrpld.app > xrpl.json xrpld.app > xrpl.resource xrpld.conditions > xrpl.basics 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 3206f3734..f16647304 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/xrpld/app/hook/applyHook.h" +APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro" { echo '// For documentation please see: https://xrpl-hooks.readme.io/reference/' @@ -19,127 +19,36 @@ APPLY_HOOK="$SCRIPT_DIR/../src/xrpld/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/include/xrpl/basics/Expected.h b/include/xrpl/basics/Expected.h index ced41b13c..db3545fc7 100644 --- a/include/xrpl/basics/Expected.h +++ b/include/xrpl/basics/Expected.h @@ -138,14 +138,14 @@ public: template requires std::convertible_to constexpr Expected(U&& r) - : Base(boost::outcome_v2::in_place_type_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(boost::outcome_v2::in_place_type_t{}, std::move(e.value())) + : Base(boost::outcome_v2::failure(E(std::move(e.value())))) { } @@ -230,7 +230,8 @@ public: template requires std::convertible_to && (!std::is_reference_v) - constexpr Expected(Unexpected e) : Base(E(std::move(e.value()))) + constexpr Expected(Unexpected e) + : Base(boost::outcome_v2::failure(E(std::move(e.value())))) { } diff --git a/include/xrpl/hook/Enum.h b/include/xrpl/hook/Enum.h index 99c7927e1..84c654f3e 100644 --- a/include/xrpl/hook/Enum.h +++ b/include/xrpl/hook/Enum.h @@ -5,6 +5,28 @@ #include #ifndef HOOKENUM_INCLUDED #define HOOKENUM_INCLUDED 1 + +#ifndef GUARD_CHECKER_BUILD +#include +#include +#include +#else +// 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(const uint256& feature) const + { + return true; + } +}; +} // namespace hook_api +#endif + namespace ripple { enum HookSetOperation : int8_t { hsoINVALID = -1, @@ -367,110 +389,59 @@ 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) \ - { \ - #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{ - // 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 -}; +inline APIWhitelist +getImportWhitelist(Rules const& rules) +{ + APIWhitelist whitelist; -// featureHooks1 -static const APIWhitelist import_whitelist_1{ - // clang-format off - HOOK_API_DEFINITION(I64, xpop_slot, (I32, I32)), - // clang-format on -}; +#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; +} #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/include/xrpl/hook/Guard.h b/include/xrpl/hook/Guard.h index f395af448..051d82128 100644 --- a/include/xrpl/hook/Guard.h +++ b/include/xrpl/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/include/xrpl/hook/HookAPI.h b/include/xrpl/hook/HookAPI.h new file mode 100644 index 000000000..fd8e46271 --- /dev/null +++ b/include/xrpl/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/include/xrpl/hook/Macro.h b/include/xrpl/hook/Macro.h index 5f309b2e3..99d1f42f7 100644 --- a/include/xrpl/hook/Macro.h +++ b/include/xrpl/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 \ @@ -190,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/include/xrpl/hook/guard_checker.cpp b/include/xrpl/hook/guard_checker.cpp index f20d24617..1fc61ea2d 100644 --- a/include/xrpl/hook/guard_checker.cpp +++ b/include/xrpl/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/include/xrpl/hook/hook_api.macro b/include/xrpl/hook/hook_api.macro new file mode 100644 index 000000000..6f76f4f8d --- /dev/null +++ b/include/xrpl/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/test/app/HookAPI_test.cpp b/src/test/app/HookAPI_test.cpp new file mode 100644 index 000000000..244280935 --- /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 5f6bf0b52..b77436608 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); } @@ -6872,7 +6914,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 +6995,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"), 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 049e8d268..0812a6de3 100644 --- a/src/test/jtx/hook.h +++ b/src/test/jtx/hook.h @@ -21,8 +21,12 @@ #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 e39af2eb7..8602e0053 100644 --- a/src/test/jtx/impl/hook.cpp +++ b/src/test/jtx/impl/hook.cpp @@ -18,8 +18,10 @@ //============================================================================== #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 diff --git a/src/xrpld/app/hook/applyHook.h b/src/xrpld/app/hook/applyHook.h index 6f53c42bf..47d34a7db 100644 --- a/src/xrpld/app/hook/applyHook.h +++ b/src/xrpld/app/hook/applyHook.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -61,365 +62,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 { @@ -477,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; @@ -554,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 @@ -794,97 +461,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/xrpld/app/hook/detail/HookAPI.cpp b/src/xrpld/app/hook/detail/HookAPI.cpp new file mode 100644 index 000000000..71f4aadca --- /dev/null +++ b/src/xrpld/app/hook/detail/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/xrpld/app/hook/detail/applyHook.cpp b/src/xrpld/app/hook/detail/applyHook.cpp index 2b0ba9f95..a48796141 100644 --- a/src/xrpld/app/hook/detail/applyHook.cpp +++ b/src/xrpld/app/hook/detail/applyHook.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -9,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -601,29 +601,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; @@ -736,121 +713,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) { @@ -1322,7 +1188,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), @@ -1518,187 +1383,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, @@ -1786,9 +1470,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) @@ -1809,107 +1492,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(); } @@ -2248,9 +1835,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; @@ -2261,30 +1847,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); @@ -2331,10 +1897,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; @@ -2355,16 +1922,12 @@ 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 - if (hookCtx.emitFailure) - return safe_cast( - hookCtx.emitFailure->getFieldU16(sfTransactionType)); - - return applyCtx.tx.getTxnType(); + return api.otxn_type(); HOOK_TEARDOWN(); } @@ -2374,118 +1937,47 @@ 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(); } // 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 - - 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(); } // 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 - - // 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(); } // 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 hookCtx.api().etxn_generation(); } // Return the current ledger sequence number -DEFINE_HOOK_FUNCNARG(int64_t, ledger_seq) +DEFINE_HOOK_FUNCTION(int64_t, ledger_seq) { HOOK_SETUP(); - return view.info().seq; + return api.ledger_seq(); HOOK_TEARDOWN(); } @@ -2503,7 +1995,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); @@ -2511,13 +2003,11 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } -DEFINE_HOOK_FUNCNARG(int64_t, ledger_last_time) +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(); } @@ -2543,27 +2033,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(); } @@ -2593,21 +2077,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(); } @@ -2617,13 +2099,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(); } @@ -2633,16 +2113,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(); } @@ -2660,68 +2135,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(); } @@ -2731,16 +2150,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(); } @@ -2755,57 +2169,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(); } @@ -2820,61 +2188,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(); } @@ -2884,34 +2202,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(); @@ -2922,42 +2225,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(); } @@ -3038,8 +2310,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) @@ -3172,8 +2443,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); } @@ -3195,8 +2466,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); } @@ -3223,7 +2494,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); } @@ -3405,317 +2676,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) @@ -3740,6 +2710,7 @@ DEFINE_HOOK_FUNCTION( hookCtx.result.emittedTxn.push(tpTrans); return result; + HOOK_TEARDOWN(); } @@ -3760,31 +2731,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); @@ -3808,13 +2758,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(); } @@ -3833,28 +2780,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); @@ -3877,17 +2814,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); @@ -3927,17 +2857,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); @@ -3950,41 +2873,22 @@ 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(); } // 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 - - 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(); } @@ -4006,7 +2910,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); @@ -4014,207 +2919,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 @@ -4232,58 +2936,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(); } @@ -4302,71 +2960,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(); } @@ -4389,11 +2988,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; @@ -4444,12 +3043,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(); } @@ -4562,116 +3162,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(); } @@ -4720,24 +3229,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(); } @@ -4763,38 +3259,28 @@ 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(); } // 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 - return view.fees().base.drops(); + return api.fee_base(); HOOK_TEARDOWN(); } @@ -4807,41 +3293,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(); } @@ -4865,68 +3325,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(); } @@ -5049,71 +3451,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, @@ -5125,33 +3470,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(); } @@ -5164,17 +3487,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(); } @@ -5191,22 +3507,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(); } @@ -5216,10 +3522,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(); } @@ -5237,46 +3542,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(); } @@ -5289,36 +3558,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(); } @@ -5382,160 +3625,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(); } @@ -5554,167 +3656,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_FUNCNARG(int64_t, float_one) +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) @@ -5722,13 +3692,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(); } @@ -5739,9 +3708,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(); } @@ -5752,62 +3723,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, @@ -5815,18 +3736,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(); } @@ -5837,21 +3750,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(); } @@ -5873,46 +3776,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(); } @@ -5934,54 +3809,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(); } @@ -6004,40 +3842,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(); } @@ -6058,75 +3886,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_FUNCNARG(int64_t, hook_pos) +DEFINE_HOOK_FUNCTION(int64_t, hook_pos) { - return hookCtx.result.hookChainPosition; + return hookCtx.api().hook_pos(); } -DEFINE_HOOK_FUNCNARG(int64_t, hook_again) +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(); } @@ -6135,30 +3919,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(); } @@ -6171,66 +3936,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/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index e0490c586..704524eda 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -607,8 +608,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/xrpld/app/tx/detail/SetHook.cpp b/src/xrpld/app/tx/detail/SetHook.cpp index a12aa281c..0d3e63ac1 100644 --- a/src/xrpld/app/tx/detail/SetHook.cpp +++ b/src/xrpld/app/tx/detail/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()) {