Compare commits

...

22 Commits

Author SHA1 Message Date
tequ
119b0221c7 format 2026-04-14 11:22:39 +09:00
tequ
aa6101409d Add Cost mechanism for Hook APIs (set 0 now) 2026-04-14 11:17:54 +09:00
tequ
6e3e518bdc fix gaspool and caluculateHookgas overflow 2026-04-03 15:24:06 +09:00
tequ
b46613fd74 Reject non-function imports (memory, global, table) 2026-04-03 14:58:33 +09:00
tequ
8aef5c10f7 Add gas price 2026-03-30 11:26:12 +09:00
tequ
5885be9f8a Add memory page size validation 2026-03-13 02:17:06 +09:00
tequ
41b87c8749 address reviews 2026-03-12 21:28:20 +09:00
tequ
3a4ca5560a Add tests for GasHook Creation/Installation/Update/Delete/NSDelete 2026-03-12 18:45:50 +09:00
tequ
563a902a5d re-sort tests 2026-03-12 16:29:11 +09:00
tequ
4491894a90 Merge branch 'dev' into gas-hook 2026-03-09 14:40:29 +09:00
tequ
efea057fad Add tests for CbakGas 2026-02-06 14:35:42 +09:00
tequ
7a9de3a205 Add test for WeakGas 2026-02-06 12:09:23 +09:00
tequ
9bdaa58d9b update sfcodes.h 2026-02-05 21:53:40 +09:00
tequ
4d6d944831 HookWeakGas/HookCallbackGas 2026-02-05 21:51:02 +09:00
tequ
cc3a63ad31 Merge remote-tracking branch 'upstream/dev' into gas-hook 2026-02-03 14:01:27 +09:00
tequ
0ff199501f Merge branch 'dev' into gas-hook 2026-01-27 21:00:23 +09:00
tequ
d8d848eb70 fix format 2026-01-24 15:28:22 +09:00
tequ
ca08b61a78 Add Gas Hooks tests for memory instruction 2026-01-24 15:10:47 +09:00
tequ
1101f99afd fix maxMemoryPage setting 2026-01-24 12:20:54 +09:00
tequ
a9b956bbd9 Add GasValidator tests 2026-01-24 11:23:17 +09:00
tequ
44ba7a01aa add Wasm validation/Memory limit 2026-01-23 19:48:08 +09:00
tequ
47beef302c Gas Type Hook 2026-01-23 16:41:15 +09:00
36 changed files with 5977 additions and 122 deletions

View File

@@ -72,6 +72,10 @@
#define sfLockCount ((2U << 16U) + 49U)
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
#define sfOracleDocumentID ((2U << 16U) + 51U)
#define sfHookCallbackGas ((2U << 16U) + 89U)
#define sfHookWeakGas ((2U << 16U) + 90U)
#define sfHookInstructionCost ((2U << 16U) + 91U)
#define sfHookGas ((2U << 16U) + 92U)
#define sfStartTime ((2U << 16U) + 93U)
#define sfRepeatCount ((2U << 16U) + 94U)
#define sfDelaySeconds ((2U << 16U) + 95U)
@@ -105,6 +109,7 @@
#define sfMPTAmount ((3U << 16U) + 26U)
#define sfIssuerNode ((3U << 16U) + 27U)
#define sfSubjectNode ((3U << 16U) + 28U)
#define sfHookGasPrice ((3U << 16U) + 96U)
#define sfTouchCount ((3U << 16U) + 97U)
#define sfAccountIndex ((3U << 16U) + 98U)
#define sfAccountCount ((3U << 16U) + 99U)

View File

@@ -266,6 +266,8 @@ enum hook_log_code : uint16_t {
CUSTOM_SECTION_DISALLOWED =
86, // the wasm contained a custom section (id=0)
INTERNAL_ERROR = 87, // an internal error described by the log text
MEMORY_PAGE_LIMIT =
88, // memory import declares min or max exceeding the page limit
// RH NOTE: only HookSet msgs got log codes, possibly all Hook log lines
// should get a code?
};
@@ -391,6 +393,7 @@ enum ExitType : uint8_t {
WASM_ERROR = 1,
ROLLBACK = 2,
ACCEPT = 3,
GAS_INSUFFICIENT = 4,
};
const uint16_t max_state_modifications = 256;
@@ -400,6 +403,8 @@ const uint8_t max_emit = 255;
const uint8_t max_params = 16;
const double fee_base_multiplier = 1.1f;
const uint8_t max_memory_pages = 8;
using APIWhitelist = std::map<std::string, std::vector<uint8_t>>;
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and
@@ -411,7 +416,9 @@ getImportWhitelist(Rules const& rules)
APIWhitelist whitelist;
#pragma push_macro("HOOK_API_DEFINITION")
#pragma push_macro("HOOK_API_COST")
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#define int64_t 0x7EU
#define int32_t 0x7FU
@@ -424,15 +431,18 @@ getImportWhitelist(Rules const& rules)
if (AMENDMENT == uint256{} || rules.enabled(AMENDMENT)) \
whitelist[#FUNCTION_NAME] = { \
RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE};
#define HOOK_API_COST(FUNCTION_NAME, cost, amendment)
#include "hook_api.macro"
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#undef HOOK_WRAP_PARAMS
#undef int64_t
#undef int32_t
#undef uint32_t
#pragma pop_macro("HOOK_API_DEFINITION")
#pragma pop_macro("HOOK_API_COST")
return whitelist;
}

View File

@@ -0,0 +1,61 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 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.
*/
//==============================================================================
#ifndef RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED
#define RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED
#include <xrpl/basics/Expected.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Rules.h>
#include <functional>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
// Forward declaration
using GuardLog =
std::optional<std::reference_wrapper<std::basic_ostream<char>>>;
namespace hook {
/**
* @brief Validate WASM host functions for Gas-type hooks
*
* Validates that a WASM binary only imports allowed host functions
* and does not import the _g (guard) function, which is only for
* Guard-type hooks.
*
* @param wasm The WASM binary to validate
* @param guardLog Logging function for validation errors
* @param guardLogAccStr Account string for logging
* @return bool if validation succeeds,
* @return true if contains cbak function
* @return false if otherwise
* @return error message if validation fails
*/
ripple::Expected<bool, std::string>
validateWasmHostFunctionsForGas(
std::vector<uint8_t> const& wasm,
ripple::Rules const& rules,
beast::Journal const& j);
} // namespace hook
#endif // RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED

View File

@@ -2,373 +2,448 @@
HOOK_API_DEFINITION(
int32_t, _g, (uint32_t, uint32_t),
uint256{})
HOOK_API_COST(_g, 0, 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{})
HOOK_API_COST(accept, 0, 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{})
HOOK_API_COST(rollback, 0, 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{})
HOOK_API_COST(util_raddr, 0, 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{})
HOOK_API_COST(util_accid, 0, 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{})
HOOK_API_COST(util_verify, 0, 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{})
HOOK_API_COST(util_sha512h, 0, 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{})
HOOK_API_COST(util_keylet, 0, 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{})
HOOK_API_COST(sto_validate, 0, 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{})
HOOK_API_COST(sto_subfield, 0, 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{})
HOOK_API_COST(sto_subarray, 0, 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{})
HOOK_API_COST(sto_emplace, 0, 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{})
HOOK_API_COST(sto_erase, 0, uint256{})
// int64_t etxn_burden();
HOOK_API_DEFINITION(
int64_t, etxn_burden, (),
uint256{})
HOOK_API_COST(etxn_burden, 0, 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{})
HOOK_API_COST(etxn_details, 0, 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{})
HOOK_API_COST(etxn_fee_base, 0, uint256{})
// int64_t etxn_reserve(uint32_t count);
HOOK_API_DEFINITION(
int64_t, etxn_reserve, (uint32_t),
uint256{})
HOOK_API_COST(etxn_reserve, 0, uint256{})
// int64_t etxn_generation();
HOOK_API_DEFINITION(
int64_t, etxn_generation, (),
uint256{})
HOOK_API_COST(etxn_generation, 0, 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{})
HOOK_API_COST(etxn_nonce, 0, 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{})
HOOK_API_COST(emit, 0, uint256{})
// int64_t float_set(int32_t exponent, int64_t mantissa);
HOOK_API_DEFINITION(
int64_t, float_set, (int32_t, int64_t),
uint256{})
HOOK_API_COST(float_set, 0, uint256{})
// int64_t float_multiply(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_multiply, (int64_t, int64_t),
uint256{})
HOOK_API_COST(float_multiply, 0, 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{})
HOOK_API_COST(float_mulratio, 0, uint256{})
// int64_t float_negate(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_negate, (int64_t),
uint256{})
HOOK_API_COST(float_negate, 0, 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{})
HOOK_API_COST(float_compare, 0, uint256{})
// int64_t float_sum(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_sum, (int64_t, int64_t),
uint256{})
HOOK_API_COST(float_sum, 0, 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{})
HOOK_API_COST(float_sto, 0, 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{})
HOOK_API_COST(float_sto_set, 0, uint256{})
// int64_t float_invert(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_invert, (int64_t),
uint256{})
HOOK_API_COST(float_invert, 0, uint256{})
// int64_t float_divide(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_divide, (int64_t, int64_t),
uint256{})
HOOK_API_COST(float_divide, 0, uint256{})
// int64_t float_one();
HOOK_API_DEFINITION(
int64_t, float_one, (),
uint256{})
HOOK_API_COST(float_one, 0, uint256{})
// int64_t float_mantissa(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_mantissa, (int64_t),
uint256{})
HOOK_API_COST(float_mantissa, 0, uint256{})
// int64_t float_sign(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_sign, (int64_t),
uint256{})
HOOK_API_COST(float_sign, 0, 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{})
HOOK_API_COST(float_int, 0, uint256{})
// int64_t float_log(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_log, (int64_t),
uint256{})
HOOK_API_COST(float_log, 0, uint256{})
// int64_t float_root(int64_t float1, uint32_t n);
HOOK_API_DEFINITION(
int64_t, float_root, (int64_t, uint32_t),
uint256{})
HOOK_API_COST(float_root, 0, uint256{})
// int64_t fee_base();
HOOK_API_DEFINITION(
int64_t, fee_base, (),
uint256{})
HOOK_API_COST(fee_base, 0, uint256{})
// int64_t ledger_seq();
HOOK_API_DEFINITION(
int64_t, ledger_seq, (),
uint256{})
HOOK_API_COST(ledger_seq, 0, uint256{})
// int64_t ledger_last_time();
HOOK_API_DEFINITION(
int64_t, ledger_last_time, (),
uint256{})
HOOK_API_COST(ledger_last_time, 0, 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{})
HOOK_API_COST(ledger_last_hash, 0, 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{})
HOOK_API_COST(ledger_nonce, 0, 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{})
HOOK_API_COST(ledger_keylet, 0, 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{})
HOOK_API_COST(hook_account, 0, 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{})
HOOK_API_COST(hook_hash, 0, 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{})
HOOK_API_COST(hook_param_set, 0, 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{})
HOOK_API_COST(hook_param, 0, uint256{})
// int64_t hook_again();
HOOK_API_DEFINITION(
int64_t, hook_again, (),
uint256{})
HOOK_API_COST(hook_again, 0, 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{})
HOOK_API_COST(hook_skip, 0, uint256{})
// int64_t hook_pos();
HOOK_API_DEFINITION(
int64_t, hook_pos, (),
uint256{})
HOOK_API_COST(hook_pos, 0, 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{})
HOOK_API_COST(slot, 0, uint256{})
// int64_t slot_clear(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_clear, (uint32_t),
uint256{})
HOOK_API_COST(slot_clear, 0, uint256{})
// int64_t slot_count(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_count, (uint32_t),
uint256{})
HOOK_API_COST(slot_count, 0, 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{})
HOOK_API_COST(slot_set, 0, uint256{})
// int64_t slot_size(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_size, (uint32_t),
uint256{})
HOOK_API_COST(slot_size, 0, 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{})
HOOK_API_COST(slot_subarray, 0, 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{})
HOOK_API_COST(slot_subfield, 0, uint256{})
// int64_t slot_type(uint32_t slot_no, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, slot_type, (uint32_t, uint32_t),
uint256{})
HOOK_API_COST(slot_type, 0, uint256{})
// int64_t slot_float(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, slot_float, (uint32_t),
uint256{})
HOOK_API_COST(slot_float, 0, 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{})
HOOK_API_COST(state_set, 0, 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{})
HOOK_API_COST(state_foreign_set, 0, 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{})
HOOK_API_COST(state, 0, 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{})
HOOK_API_COST(state_foreign, 0, 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{})
HOOK_API_COST(trace, 0, 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{})
HOOK_API_COST(trace_num, 0, 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{})
HOOK_API_COST(trace_float, 0, uint256{})
// int64_t otxn_burden();
HOOK_API_DEFINITION(
int64_t, otxn_burden, (),
uint256{})
HOOK_API_COST(otxn_burden, 0, 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{})
HOOK_API_COST(otxn_field, 0, uint256{})
// int64_t otxn_generation();
HOOK_API_DEFINITION(
int64_t, otxn_generation, (),
uint256{})
HOOK_API_COST(otxn_generation, 0, 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{})
HOOK_API_COST(otxn_id, 0, uint256{})
// int64_t otxn_type();
HOOK_API_DEFINITION(
int64_t, otxn_type, (),
uint256{})
HOOK_API_COST(otxn_type, 0, uint256{})
// int64_t otxn_slot(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, otxn_slot, (uint32_t),
uint256{})
HOOK_API_COST(otxn_slot, 0, 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{})
HOOK_API_COST(otxn_param, 0, uint256{})
// int64_t meta_slot(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, meta_slot, (uint32_t),
uint256{})
HOOK_API_COST(meta_slot, 0, 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)
HOOK_API_COST(xpop_slot, 0, uint256{})
// int64_t prepare(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
featureHooksUpdate2)
HOOK_API_COST(prepare, 0, uint256{})

View File

@@ -80,7 +80,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 113;
static constexpr std::size_t numFeatures = 114;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated

View File

@@ -34,6 +34,8 @@ struct Fees
XRPAmount base{0}; // Reference tx cost (drops)
XRPAmount reserve{0}; // Reserve base (drops)
XRPAmount increment{0}; // Reserve increment (drops)
std::uint64_t hookGasPrice{
0}; // Gas price for gas-type hooks (micro-drops per gas unit)
explicit Fees() = default;
Fees(Fees const&) = default;

View File

@@ -362,6 +362,8 @@ enum TECcodes : TERUnderlyingType {
tecARRAY_TOO_LARGE = 197,
tecLOCKED = 198,
tecBAD_CREDENTIALS = 199,
tecHOOK_INSUFFICIENT_GAS = 200,
tecHOOK_INVALID = 201,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -31,6 +31,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(HookGas, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -103,10 +103,12 @@ LEDGER_ENTRY(ltHOOK_DEFINITION, 'D', HookDefinition, hook_definition, ({
{sfCreateCode, soeREQUIRED},
{sfHookSetTxnID, soeREQUIRED},
{sfReferenceCount, soeREQUIRED},
{sfFee, soeREQUIRED},
{sfFee, soeOPTIONAL},
{sfHookCallbackFee, soeOPTIONAL},
{sfPreviousTxnID, soeOPTIONAL},
{sfPreviousTxnLgrSeq, soeOPTIONAL},
{sfHookCallbackGas, soeOPTIONAL},
{sfHookWeakGas, soeOPTIONAL},
}))
/** A ledger object containing a hook-emitted transaction from a previous hook execution.
@@ -414,6 +416,7 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
{sfHookGasPrice, soeOPTIONAL},
{sfXahauActivationLgrSeq, soeOPTIONAL},
{sfAccountCount, soeOPTIONAL},
{sfNetworkID, soeOPTIONAL},

View File

@@ -115,6 +115,10 @@ TYPED_SFIELD(sfLockCount, UINT32, 49)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfHookCallbackGas, UINT32, 89)
TYPED_SFIELD(sfHookWeakGas, UINT32, 90)
TYPED_SFIELD(sfHookInstructionCost, UINT32, 91)
TYPED_SFIELD(sfHookGas, UINT32, 92)
TYPED_SFIELD(sfStartTime, UINT32, 93)
TYPED_SFIELD(sfRepeatCount, UINT32, 94)
TYPED_SFIELD(sfDelaySeconds, UINT32, 95)
@@ -153,6 +157,7 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie
TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfHookGasPrice, UINT64, 96)
TYPED_SFIELD(sfTouchCount, UINT64, 97)
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
TYPED_SFIELD(sfAccountCount, UINT64, 99)

View File

@@ -583,6 +583,7 @@ TRANSACTION(ttFEE, 101, SetFee, ({
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
{sfHookGasPrice, soeOPTIONAL},
}))
/** This system-generated transaction type is used to update the network's negative UNL

View File

@@ -87,6 +87,8 @@ JSS(HookParameterName); // field
JSS(HookParameterValue); // field
JSS(HookParameter); // field
JSS(HookGrant); // field
JSS(HookCallbackGas); // field
JSS(HookWeakGas); // field
JSS(isSerialized); // out: RPC server_definitions
// matches definitions.json format
JSS(isSigningField); // out: RPC server_definitions

View File

@@ -78,7 +78,10 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfFlags, soeOPTIONAL}});
{sfFlags, soeOPTIONAL},
{sfHookInstructionCost, soeOPTIONAL},
{sfHookWeakGas, soeOPTIONAL},
{sfHookCallbackGas, soeOPTIONAL}});
add(sfHookEmission.jsonName,
sfHookEmission.getCode(),
@@ -98,7 +101,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeREQUIRED},
{sfFlags, soeREQUIRED},
{sfFee, soeREQUIRED}});
{sfFee, soeOPTIONAL}});
add(sfHook.jsonName,
sfHook.getCode(),
@@ -112,6 +115,8 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookOnOutgoing, soeOPTIONAL},
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeOPTIONAL},
{sfHookCallbackGas, soeOPTIONAL},
{sfHookWeakGas, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});
add(sfHookGrant.jsonName,

View File

@@ -66,6 +66,8 @@ STValidation::validationFormat()
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
// featureHookGas
{sfHookGasPrice, soeOPTIONAL},
};
// clang-format on

View File

@@ -124,6 +124,8 @@ transResults()
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
MAKE_ERROR(tecLOCKED, "Fund is locked."),
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
MAKE_ERROR(tecHOOK_INSUFFICIENT_GAS, "Insufficient hook gas to complete the transaction."),
MAKE_ERROR(tecHOOK_INVALID, "Invalid hook."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),

View File

@@ -48,6 +48,7 @@ TxFormats::TxFormats()
{sfFirstLedgerSequence, soeOPTIONAL},
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookGas, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")

View File

@@ -29,6 +29,7 @@ class FeeVote_test : public beast::unit_test::suite
void
testSetup()
{
testcase("setup");
FeeSetup const defaultSetup;
{
// defaults
@@ -37,36 +38,42 @@ class FeeVote_test : public beast::unit_test::suite
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve);
BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve);
BEAST_EXPECT(setup.hook_gas_price == defaultSetup.hook_gas_price);
}
{
Section config;
config.append(
{"reference_fee = 50",
"account_reserve = 1234567",
"owner_reserve = 1234"});
"owner_reserve = 1234",
"hook_gas_price = 2000000"});
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == 50);
BEAST_EXPECT(setup.account_reserve == 1234567);
BEAST_EXPECT(setup.owner_reserve == 1234);
BEAST_EXPECT(setup.hook_gas_price == 2'000'000);
}
{
Section config;
config.append(
{"reference_fee = blah",
"account_reserve = yada",
"owner_reserve = foo"});
"owner_reserve = foo",
"hook_gas_price =invalid"});
// Illegal values are ignored, and the defaults left unchanged
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve);
BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve);
BEAST_EXPECT(setup.hook_gas_price == defaultSetup.hook_gas_price);
}
{
Section config;
config.append(
{"reference_fee = -50",
"account_reserve = -1234567",
"owner_reserve = -1234"});
"owner_reserve = -1234",
"hook_gas_price = -2000000"});
// Illegal values are ignored, and the defaults left unchanged
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
@@ -74,22 +81,30 @@ class FeeVote_test : public beast::unit_test::suite
setup.account_reserve == static_cast<std::uint32_t>(-1234567));
BEAST_EXPECT(
setup.owner_reserve == static_cast<std::uint32_t>(-1234));
BEAST_EXPECT(
setup.hook_gas_price == static_cast<std::int64_t>(-2000000));
}
{
const auto big64 = std::to_string(
const auto big64xrp = std::to_string(
static_cast<std::uint64_t>(
std::numeric_limits<XRPAmount::value_type>::max()) +
1);
const auto big64 = std::to_string(
static_cast<std::uint64_t>(
std::numeric_limits<std::int64_t>::max()) +
1);
Section config;
config.append(
{"reference_fee = " + big64,
"account_reserve = " + big64,
"owner_reserve = " + big64});
{"reference_fee = " + big64xrp,
"account_reserve = " + big64xrp,
"owner_reserve = " + big64xrp,
"hook_gas_price =" + big64});
// Illegal values are ignored, and the defaults left unchanged
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve);
BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve);
BEAST_EXPECT(setup.hook_gas_price == defaultSetup.hook_gas_price);
}
}

View File

@@ -0,0 +1,260 @@
//------------------------------------------------------------------------------
/*
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 <test/jtx.h>
#include <xrpld/app/misc/FeeVote.h>
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
namespace ripple {
namespace test {
class HookGasPrice_test : public beast::unit_test::suite
{
// Advance to flag ledger so fee voting sets sfHookGasPrice
void
advanceToFlagLedger(jtx::Env& env)
{
auto const seq = env.current()->info().seq;
for (auto i = seq; i <= 256; ++i)
env.close();
env.close();
}
void
testHookGasPriceGenesis(FeatureBitset features)
{
testcase("GasPrice genesis");
using namespace jtx;
// Test Env passes empty amendments to genesis, so sfHookGasPrice
// is NOT set in the genesis FeeSettings SLE.
{
Env env{*this, features};
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(!sle->isFieldPresent(sfHookGasPrice));
}
// Without featureHookGas - also no sfHookGasPrice
{
Env env{*this, features - featureHookGas};
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(!sle->isFieldPresent(sfHookGasPrice));
}
}
void
testHookGasPriceFeeVoting(FeatureBitset features)
{
testcase("GasPrice fee voting");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(10000), Account{"alice"});
env.close();
// Before flag ledger: sfHookGasPrice not in SLE
{
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(!sle->isFieldPresent(sfHookGasPrice));
}
// Advance to flag ledger to trigger fee voting
advanceToFlagLedger(env);
// After fee voting with featureHookGas enabled:
// sfHookGasPrice should be set in SLE via ttFEE pseudo-transaction
{
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(sle->isFieldPresent(sfHookGasPrice));
if (sle->isFieldPresent(sfHookGasPrice))
BEAST_EXPECT(sle->getFieldU64(sfHookGasPrice) == 1'000'000);
}
BEAST_EXPECT(
env.current()->fees().hookGasPrice == XRPAmount{1'000'000});
}
void
testHookGasPriceVotingUpdate(FeatureBitset features)
{
testcase("GasPrice voting update");
using namespace jtx;
// Create env with custom gas_price = 2,000,000 via voting config
auto cfg = envconfig();
auto& votingSection = cfg->section("voting");
votingSection.set("hook_gas_price", "2000000");
// A validation_seed is required for fee voting to work
cfg->section("validation_seed").legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH");
Env env{*this, std::move(cfg), features};
env.fund(XRP(10000), Account{"alice"});
env.close();
// Before flag ledger: sfHookGasPrice not in SLE
{
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(!sle->isFieldPresent(sfHookGasPrice));
}
// Advance to flag ledger to trigger fee voting
advanceToFlagLedger(env);
// After voting: sfHookGasPrice should be updated to 2,000,000
{
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(sle->isFieldPresent(sfHookGasPrice));
if (sle->isFieldPresent(sfHookGasPrice))
BEAST_EXPECT(sle->getFieldU64(sfHookGasPrice) == 2'000'000);
}
}
void
testHookGasPriceFeeCalculation(FeatureBitset features)
{
testcase("GasPrice fee calculation");
using namespace jtx;
{
// With default gasPrice = 1,000,000:
auto cfg = envconfig();
auto& votingSection = cfg->section("voting");
votingSection.set("hook_gas_price", "1000000");
// A validation_seed is required for fee voting to work
cfg->section("validation_seed")
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH");
Env env{*this, std::move(cfg), features};
env.fund(XRP(10000), Account{"alice"});
env.close();
// Advance to flag ledger so gasPrice is set via fee voting
advanceToFlagLedger(env);
auto const& fees = env.current()->fees();
BEAST_EXPECT(fees.hookGasPrice == XRPAmount{1'000'000});
// Verify the gas price calculation formula:
// fee = gasCount * gasPrice / 1,000,000
// With default gasPrice = 1,000,000:
// 1,000,000 gas => 1,000,000 * 1,000,000 / 1,000,000 = 1,000,000
// 500,000 gas => 500,000 drops
// 0 gas => 0 drops
for (auto gasCount : {1'000'000, 500'000, 0})
{
auto gasFee = Transactor::calculateHookGas(gasCount, fees);
BEAST_EXPECT(gasFee == XRPAmount{gasCount});
}
}
{
// With gasPrice = 100,000:
auto cfg = envconfig();
auto& votingSection = cfg->section("voting");
votingSection.set("hook_gas_price", "100000");
// A validation_seed is required for fee voting to work
cfg->section("validation_seed")
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH");
Env env{*this, std::move(cfg), features};
env.fund(XRP(10000), Account{"alice"});
env.close();
// Advance to flag ledger so gasPrice is set via fee voting
advanceToFlagLedger(env);
auto const& fees = env.current()->fees();
BEAST_EXPECT(fees.hookGasPrice == XRPAmount{100'000});
// Verify the gas price calculation formula:
// fee = gasCount * gasPrice / 1,000,000
// With default gasPrice = 1,000,000:
// 1,000,000 gas => 1,000,000 * 100,000 / 1,000,000 = 100,000
// 500,000 gas => 50,000 drops
// 0 gas => 0 drops
for (auto gasCount : {1'000'000, 500'000, 0})
{
auto gasFee = Transactor::calculateHookGas(gasCount, fees);
BEAST_EXPECT(gasFee == XRPAmount{gasCount / 10});
}
}
}
void
testHookGasPriceDisabled(FeatureBitset features)
{
testcase("GasPrice disabled");
using namespace jtx;
Env env{*this, features - featureHookGas};
env.fund(XRP(10000), Account{"alice"});
env.close();
// sfHookGasPrice not in SLE when amendment disabled
auto const sle = env.le(keylet::fees());
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(!sle->isFieldPresent(sfHookGasPrice));
// Advance past flag ledger - sfHookGasPrice should still not be set
advanceToFlagLedger(env);
{
auto const sle2 = env.le(keylet::fees());
if (!BEAST_EXPECT(sle2))
return;
BEAST_EXPECT(!sle2->isFieldPresent(sfHookGasPrice));
}
}
void
run() override
{
using namespace jtx;
auto const sa = supported_amendments();
testHookGasPriceGenesis(sa);
testHookGasPriceFeeVoting(sa);
testHookGasPriceVotingUpdate(sa);
testHookGasPriceFeeCalculation(sa);
testHookGasPriceDisabled(sa);
}
};
BEAST_DEFINE_TESTSUITE(HookGasPrice, app, ripple);
} // namespace test
} // namespace ripple

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -41,17 +41,28 @@ echo '
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> wasm = {' > $OUTPUT_FILE
COUNTER="0"
cat $INPUT_FILE | tr '\n' '\f' |
grep -Po 'R"\[test\.hook\](.*?)\[test\.hook\]"' |
sed -E 's/R"\[test\.hook\]\(//g' |
sed -E 's/\)\[test\.hook\]"[\f \t]*/\/*end*\//g' |
# Counter file for sharing between subshells
COUNTER_FILE=$(mktemp)
echo "0" > $COUNTER_FILE
trap "rm -f $COUNTER_FILE" EXIT
# Process both [test.hook] and [test.hook.gas] blocks
process_block() {
local tag_pattern="$1" # regex pattern: "hook" or "hook\.gas"
local tag_output="$2" # output string: "hook" or "hook.gas"
local skip_cleaner="$3" # "0" for no skip, "1" for skip
cat $INPUT_FILE | tr '\n' '\f' |
grep -Po "R\"\[test\.${tag_pattern}\](.*?)\[test\.${tag_pattern}\]\"" |
sed -E "s/R\"\[test\.${tag_pattern}\]\(//g" |
sed -E "s/\)\[test\.${tag_pattern}\]\"[\f \t]*/\/*end*\//g" |
while read -r line
do
COUNTER=$(cat $COUNTER_FILE)
echo "/* ==== WASM: $COUNTER ==== */" >> $OUTPUT_FILE
echo -n '{ R"[test.hook](' >> $OUTPUT_FILE
echo -n "{ R\"[test.${tag_output}](" >> $OUTPUT_FILE
cat <<< "$line" | sed -E 's/.{7}$//g' | tr -d '\n' | tr '\f' '\n' >> $OUTPUT_FILE
echo ')[test.hook]",' >> $OUTPUT_FILE
echo ")[test.${tag_output}]\"," >> $OUTPUT_FILE
echo "{" >> $OUTPUT_FILE
WAT=`grep -Eo '\(module' <<< $line | wc -l`
if [ "$WAT" -eq "0" ]
@@ -69,10 +80,19 @@ cat $INPUT_FILE | tr '\n' '\f' |
echo "$line"
exit 1
fi
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< "`tr '\f' '\n' <<< $line`" |
hook-cleaner - - 2>/dev/null |
xxd -p -u -c 10 |
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
if [ "$skip_cleaner" -eq "1" ]
then
# Skip hook-cleaner for [test.hook.gas]
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined -Wno-int-conversion -Wno-pointer-sign -Wno-return-type <<< "`tr '\f' '\n' <<< $line`" |
xxd -p -u -c 10 |
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
else
# Run hook-cleaner for [test.hook]
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined -Wno-int-conversion -Wno-pointer-sign -Wno-return-type <<< "`tr '\f' '\n' <<< $line`" |
hook-cleaner - - 2>/dev/null |
xxd -p -u -c 10 |
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
fi
else
wat2wasm - -o /dev/stdout <<< "`tr '\f' '\n' <<< $(sed -E 's/.{7}$//g' <<< $line)`" |
xxd -p -u -c 10 |
@@ -85,8 +105,15 @@ cat $INPUT_FILE | tr '\n' '\f' |
fi
echo '}},' >> $OUTPUT_FILE
echo >> $OUTPUT_FILE
COUNTER=`echo $COUNTER + 1 | bc`
echo $((COUNTER + 1)) > $COUNTER_FILE
done
}
# Process [test.hook] blocks (with hook-cleaner)
process_block "hook" "hook" "0"
# Process [test.hook.gas] blocks (without hook-cleaner)
process_block "hook\.gas" "hook.gas" "1"
echo '};
}
}

View File

@@ -44,6 +44,9 @@ hso(std::vector<uint8_t> const& wasmBytes, void (*f)(Json::Value& jv) = 0);
Json::Value
hso(std::string const& wasmHex, void (*f)(Json::Value& jv) = 0);
Json::Value
hso(uint256 const& hookHash, void (*f)(Json::Value& jv) = 0);
Json::Value
hso_delete(void (*f)(Json::Value& jv) = 0);

80
src/test/jtx/hookgas.h Normal file
View File

@@ -0,0 +1,80 @@
//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_HOOKGAS_H_INCLUDED
#define RIPPLE_TEST_JTX_HOOKGAS_H_INCLUDED
#include <test/jtx/Env.h>
#include <test/jtx/tags.h>
#include <xrpl/basics/contract.h>
namespace ripple {
namespace test {
namespace jtx {
/** Set the HookGas on a JTx. */
class hookgas
{
private:
std::uint32_t gas_;
public:
hookgas(std::uint32_t gas) : gas_{gas}
{
}
void
operator()(Env&, JTx& jt) const;
};
/** Set the HookCallbackGas on a JTx. */
class cbakgas
{
private:
std::uint32_t gas_;
public:
cbakgas(std::uint32_t gas) : gas_{gas}
{
}
void
operator()(Env&, JTx& jt) const;
};
/** Set the HookWeakGas on a JTx. */
class weakgas
{
private:
std::uint32_t gas_;
public:
weakgas(std::uint32_t gas) : gas_{gas}
{
}
void
operator()(Env&, JTx& jt) const;
};
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -104,6 +104,16 @@ hso(std::string const& wasmHex, void (*f)(Json::Value& jv))
return jv;
}
Json::Value
hso(uint256 const& hookHash, void (*f)(Json::Value& jv))
{
Json::Value jv;
jv[jss::HookHash] = to_string(hookHash);
if (f)
f(jv);
return jv;
}
// Helper function to create HookContext with external stateMap
hook::HookContext
makeStubHookContext(

View File

@@ -0,0 +1,47 @@
//------------------------------------------------------------------------------
/*
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 <test/jtx/hookgas.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
namespace jtx {
void
hookgas::operator()(Env&, JTx& jt) const
{
jt[sfHookGas.jsonName] = gas_;
}
void
cbakgas::operator()(Env&, JTx& jt) const
{
jt[sfHookCallbackGas.jsonName] = gas_;
}
void
weakgas::operator()(Env&, JTx& jt) const
{
jt[sfHookWeakGas.jsonName] = gas_;
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -63,18 +63,23 @@ namespace hook_api {
fprintf
#pragma push_macro("HOOK_API_DEFINITION")
#pragma push_macro("HOOK_API_COST")
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#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);
#define HOOK_API_COST(FUNCTION_NAME, cost, amendment)
#include <xrpl/hook/hook_api.macro>
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#undef HOOK_WRAP_PARAMS
#pragma pop_macro("HOOK_API_DEFINITION")
#pragma pop_macro("HOOK_API_COST")
} /* end namespace hook_api */
@@ -124,7 +129,9 @@ apply(
uint32_t wasmParam,
uint8_t hookChainPosition,
// result of apply() if this is weak exec
std::shared_ptr<STObject const> const& provisionalMeta);
std::shared_ptr<STObject const> const& provisionalMeta,
uint16_t hookApiVersion,
uint32_t hookGas);
struct HookContext;
@@ -162,6 +169,7 @@ struct HookResult
std::string exitReason{""};
int64_t exitCode{-1};
uint64_t instructionCount{0};
uint64_t instructionCost{0};
bool hasCallback = false; // true iff this hook wasm has a cbak function
bool isCallback =
false; // true iff this hook execution is a callback in action
@@ -174,6 +182,8 @@ struct HookResult
false; // hook_again allows strong pre-apply to nominate
// additional weak post-apply execution
std::shared_ptr<STObject const> provisionalMeta;
uint16_t hookApiVersion = 0; // 0 = Guard-type, 1 = Gas-type
uint32_t hookGas; // Gas limit for Gas-type hooks
};
class HookExecutor;
@@ -270,14 +280,14 @@ gatherHookParameters(
beast::Journal const& j_);
// RH TODO: call destruct for these on rippled shutdown
#define ADD_HOOK_FUNCTION(F, ctx) \
#define ADD_HOOK_FUNCTION(F, ctx, cost) \
{ \
WasmEdge_FunctionInstanceContext* hf = \
WasmEdge_FunctionInstanceCreate( \
hook_api::WasmFunctionType##F, \
hook_api::WasmFunction##F, \
(void*)(&ctx), \
0); \
cost); \
WasmEdge_ModuleInstanceAddFunction( \
importObj, hook_api::WasmFunctionName##F, hf); \
}
@@ -325,12 +335,18 @@ public:
WasmEdge_ConfigureContext* conf = NULL;
WasmEdge_VMContext* ctx = NULL;
WasmEdgeVM()
WasmEdgeVM(uint16_t hookApiVersion)
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
if (hookApiVersion == 1)
{
WasmEdge_ConfigureStatisticsSetCostMeasuring(conf, true);
WasmEdge_ConfigureSetMaxMemoryPage(
conf, hook_api::max_memory_pages);
}
ctx = WasmEdge_VMCreate(conf, NULL);
}
@@ -365,9 +381,9 @@ public:
* Validate that a web assembly blob can be loaded by wasmedge
*/
static std::optional<std::string>
validateWasm(const void* wasm, size_t len)
validateWasm(const void* wasm, size_t len, uint16_t hookApiVersion)
{
WasmEdgeVM vm;
WasmEdgeVM vm{hookApiVersion};
if (!vm.sane())
return "Could not create WASMEDGE instance";
@@ -412,7 +428,7 @@ public:
WasmEdge_LogOff();
WasmEdgeVM vm;
WasmEdgeVM vm{hookCtx.result.hookApiVersion};
if (!vm.sane())
{
@@ -433,6 +449,22 @@ public:
return;
}
// Set Gas limit for Gas-type hooks (HookApiVersion == 1)
if (hookCtx.result.hookApiVersion == 1)
{
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
if (statsCtx)
{
// Convert HookGas to cost limit count (1 Gas = 1 cost)
uint32_t gasLimit = hookCtx.result.hookGas;
WasmEdge_StatisticsSetCostLimit(statsCtx, gasLimit);
JLOG(j.trace())
<< "HookInfo[" << HC_ACC() << "]: Set Gas limit to "
<< gasLimit << " cost limit for Gas-type Hook";
}
}
WasmEdge_Value params[1] = {WasmEdge_ValueGenI32((int64_t)wasmParam)};
WasmEdge_Value returns[1];
@@ -446,16 +478,31 @@ public:
returns,
1);
if (auto err = getWasmError("WASM VM error", res); err)
{
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: " << *err;
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
return;
}
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
hookCtx.result.instructionCount =
WasmEdge_StatisticsGetInstrCount(statsCtx);
hookCtx.result.instructionCost =
WasmEdge_StatisticsGetTotalCost(statsCtx);
if (auto err = getWasmError("WASM VM error", res); err)
{
JLOG(j.trace()) << "HookError[" << HC_ACC() << "]: " << *err;
// Check if error is due to Gas limit exceeded for Gas-type hooks
if (hookCtx.result.hookApiVersion == 1 &&
err->find("cost limit exceeded") != std::string::npos)
{
JLOG(j.trace()) << "HookError[" << HC_ACC()
<< "]: Gas limit exceeded. Limit was "
<< hookCtx.result.hookGas;
hookCtx.result.exitType = hook_api::ExitType::GAS_INSUFFICIENT;
}
else
{
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
}
return;
}
// RH NOTE: stack unwind will clean up WasmEdgeVM
}
@@ -468,17 +515,40 @@ public:
WasmEdge_LogSetDebugLevel();
#pragma push_macro("HOOK_API_DEFINITION")
#pragma push_macro("HOOK_API_COST")
// Access rules for amendment-based cost switching
auto const& rules_ = ctx.applyCtx.view().rules();
// Phase 1: Declare per-function cost variables, initialized to 0
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, ...) \
ADD_HOOK_FUNCTION(FUNCTION_NAME, ctx);
#define HOOK_API_DEFINITION(RT, FN, ...) uint64_t cost_##FN = 0;
#define HOOK_API_COST(...)
#include <xrpl/hook/hook_api.macro>
// Phase 2: Set costs; amendment-gated entries override base costs
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#define HOOK_API_DEFINITION(...)
#define HOOK_API_COST(FN, cost, AM) \
if ((AM) == uint256{} || rules_.enabled(AM)) \
cost_##FN = (cost);
#include <xrpl/hook/hook_api.macro>
// Phase 3: Register functions with WasmEdge using computed costs
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#define HOOK_API_DEFINITION(RT, FN, ...) ADD_HOOK_FUNCTION(FN, ctx, cost_##FN);
#define HOOK_API_COST(...)
#include <xrpl/hook/hook_api.macro>
#undef HOOK_API_DEFINITION
#undef HOOK_API_COST
#undef HOOK_WRAP_PARAMS
#pragma pop_macro("HOOK_API_DEFINITION")
#pragma pop_macro("HOOK_API_COST")
WasmEdge_TableInstanceContext* hostTable =
WasmEdge_TableInstanceCreate(tableType);

View File

@@ -0,0 +1,444 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 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 <xrpl/basics/Expected.h>
#include <xrpl/basics/Log.h>
#include <xrpl/hook/GasValidator.h>
#include <xrpl/hook/Guard.h>
#include <xrpl/hook/Macro.h>
#include <xrpl/protocol/Feature.h>
#include <wasmedge/wasmedge.h>
namespace hook {
Expected<bool, std::string>
validateExportSection(
WasmEdge_ASTModuleContext* astModule,
beast::Journal const& j)
{
// Get export count
uint32_t exportCount = WasmEdge_ASTModuleListExportsLength(astModule);
if (exportCount == 0)
{
return Unexpected("WASM must export at least hook API functions");
}
// Get exports
const WasmEdge_ExportTypeContext* exports[256];
uint32_t actualExportCount = std::min(exportCount, 256u);
actualExportCount =
WasmEdge_ASTModuleListExports(astModule, exports, actualExportCount);
// Track if we found required hook() function
bool foundHook = false;
bool foundCbak = false;
// Check each export
for (uint32_t i = 0; i < actualExportCount; i++)
{
WasmEdge_ExternalType const type =
WasmEdge_ExportTypeGetExternalType(exports[i]);
// Validate memory exports for page limits
if (type == WasmEdge_ExternalType_Memory)
{
const WasmEdge_MemoryTypeContext* memType =
WasmEdge_ExportTypeGetMemoryType(astModule, exports[i]);
if (!memType)
{
JLOG(j.trace()) << "HookSet(" << hook::log::MEMORY_PAGE_LIMIT
<< "): Exported memory has no type definition";
return Unexpected("Exported memory has no type definition");
}
WasmEdge_Limit limit = WasmEdge_MemoryTypeGetLimit(memType);
constexpr uint32_t kMaxMemoryPages = hook_api::max_memory_pages;
if (limit.Min > kMaxMemoryPages)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::MEMORY_PAGE_LIMIT
<< "): Gas-type hook exported memory minimum "
"pages ("
<< limit.Min << ") exceeds limit of " << kMaxMemoryPages;
return Unexpected(
"Gas-type hook exported memory minimum pages "
"exceed limit of 8");
}
if (limit.HasMax && limit.Max > kMaxMemoryPages)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::MEMORY_PAGE_LIMIT
<< "): Gas-type hook exported memory maximum "
"pages ("
<< limit.Max << ") exceeds limit of " << kMaxMemoryPages;
return Unexpected(
"Gas-type hook exported memory maximum pages "
"exceed limit of 8");
}
continue;
}
// Only check function exports
if (type != WasmEdge_ExternalType_Function)
continue;
WasmEdge_String const name =
WasmEdge_ExportTypeGetExternalName(exports[i]);
std::string nameStr(name.Buf, name.Length);
if (nameStr.starts_with("__"))
{
// skip runtime support functions
continue;
}
// Only allow hook() and cbak() exports
if (nameStr != "hook" && nameStr != "cbak")
{
JLOG(j.trace()) << "HookSet(" << hook::log::EXPORT_MISSING
<< "): Unauthorized export function '" << nameStr
<< "'. Only 'hook' and 'cbak' are allowed";
return Unexpected(
"Unauthorized export function '" + nameStr +
"'. Only 'hook' and 'cbak' are allowed");
}
if (nameStr == "hook")
foundHook = true;
if (nameStr == "cbak")
foundCbak = true;
// Get function type to validate signature
WasmEdge_FunctionTypeContext const* functionType =
WasmEdge_ExportTypeGetFunctionType(astModule, exports[i]);
// Validate parameter count (must be exactly 1)
uint32_t paramCount =
WasmEdge_FunctionTypeGetParametersLength(functionType);
if (paramCount != 1)
{
JLOG(j.trace())
<< "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' must have exactly 1 parameter, found " << paramCount;
return Unexpected(
"Function '" + nameStr +
"' must have exactly 1 parameter of type uint32_t");
}
// Validate parameter type (must be i32 / uint32_t)
WasmEdge_ValType parameters[1];
WasmEdge_FunctionTypeGetParameters(functionType, parameters, 1);
if (parameters[0] != WasmEdge_ValType_I32)
{
JLOG(j.trace()) << "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' parameter must be uint32_t (i32), found type "
<< parameters[0];
return Unexpected(
"Function '" + nameStr + "' parameter must be uint32_t (i32)");
}
// Validate return type (must be i64 / uint64_t)
uint32_t returnCount =
WasmEdge_FunctionTypeGetReturnsLength(functionType);
if (returnCount != 1)
{
JLOG(j.trace())
<< "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' must return exactly 1 value, found " << returnCount;
return Unexpected(
"Function '" + nameStr +
"' must return exactly 1 value of type uint64_t");
}
WasmEdge_ValType returns[1];
WasmEdge_FunctionTypeGetReturns(functionType, returns, 1);
if (returns[0] != WasmEdge_ValType_I64)
{
JLOG(j.trace())
<< "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' return type must be uint64_t (i64), found type "
<< returns[0];
return Unexpected(
"Function '" + nameStr +
"' return type must be uint64_t (i64)");
}
}
// Ensure hook() function was exported (required)
if (!foundHook)
{
JLOG(j.trace()) << "HookSet(" << hook::log::EXPORT_MISSING
<< "): Required function 'hook' not found in exports";
return Unexpected("Required function 'hook' not found in exports");
}
return foundCbak;
}
Expected<void, std::string>
validateImportSection(
WasmEdge_ASTModuleContext* astModule,
Rules const& rules,
beast::Journal const& j)
{
// Get import count
uint32_t importCount = WasmEdge_ASTModuleListImportsLength(astModule);
if (importCount == 0)
{
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORTS_MISSING
<< "): WASM must import at least hook API functions";
return Unexpected("WASM must import at least hook API functions");
}
// Get imports (max 256)
const WasmEdge_ImportTypeContext* imports[256];
uint32_t actualImportCount = std::min(importCount, 256u);
actualImportCount =
WasmEdge_ASTModuleListImports(astModule, imports, actualImportCount);
std::optional<std::string> error;
// Check each import
for (uint32_t i = 0; i < actualImportCount; i++)
{
WasmEdge_String moduleName =
WasmEdge_ImportTypeGetModuleName(imports[i]);
WasmEdge_String externalName =
WasmEdge_ImportTypeGetExternalName(imports[i]);
WasmEdge_ExternalType extType =
WasmEdge_ImportTypeGetExternalType(imports[i]);
// Reject non-function imports (memory, global, table)
if (extType != WasmEdge_ExternalType_Function)
{
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORT_ILLEGAL
<< "): Gas-type hooks cannot have non-function "
"imports";
return Unexpected(
"Gas-type hooks cannot have non-function imports");
}
// Convert WasmEdge_String to std::string for comparison
std::string modName(moduleName.Buf, moduleName.Length);
std::string extName(externalName.Buf, externalName.Length);
// Check module name is "env"
if (modName != "env")
{
JLOG(j.trace())
<< "HookSet(" << hook::log::IMPORT_MODULE_ENV
<< "): Import module must be 'env', found: " << modName;
return Unexpected("Import module must be 'env', found: " + modName);
}
// Check for forbidden _g function (guard function)
if (extName == "_g")
{
JLOG(j.trace())
<< "HookSet(" << hook::log::IMPORT_ILLEGAL
<< "): Gas-type hooks cannot import _g (guard) function";
return Unexpected(
"Gas-type hooks cannot import _g (guard) function");
}
// Determine which whitelist contains the function and get expected
// signature
std::vector<uint8_t> const* expectedSig = nullptr;
auto importWhitelist = hook_api::getImportWhitelist(rules);
auto baseIt = importWhitelist.find(extName);
if (baseIt != importWhitelist.end())
expectedSig = &baseIt->second;
// Function not in any whitelist
if (!expectedSig)
{
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORT_ILLEGAL
<< "): Import not in whitelist: " << extName;
return Unexpected("Import not in whitelist: " + extName);
}
// Get function type for signature validation
WasmEdge_FunctionTypeContext const* functionType =
WasmEdge_ImportTypeGetFunctionType(astModule, imports[i]);
if (!functionType)
{
JLOG(j.trace()) << "HookSet(" << hook::log::FUNC_TYPELESS
<< "): Import function '" << extName
<< "' has no function type definition";
return Unexpected(
"Import function '" + extName +
"' has no function type definition");
}
// Validate return type
// expectedSig[0] is the return type
uint32_t returnCount =
WasmEdge_FunctionTypeGetReturnsLength(functionType);
if (returnCount != 1)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::FUNC_RETURN_COUNT
<< "): Import function '" << extName
<< "' must return exactly 1 value, found " << returnCount;
return Unexpected(
"Import function '" + extName +
"' must return exactly 1 value");
}
WasmEdge_ValType actualReturnType;
WasmEdge_FunctionTypeGetReturns(functionType, &actualReturnType, 1);
if (actualReturnType != (*expectedSig)[0])
{
JLOG(j.trace()) << "HookSet(" << hook::log::FUNC_RETURN_INVALID
<< "): Import function '" << extName
<< "' has incorrect return type. Expected "
<< static_cast<int>((*expectedSig)[0]) << ", found "
<< static_cast<int>(actualReturnType);
return Unexpected(
"Import function '" + extName + "' has incorrect return type");
}
// Validate parameter count and types
// expectedSig[1..N] are the parameter types
uint32_t expectedParamCount =
expectedSig->size() > 0 ? expectedSig->size() - 1 : 0;
uint32_t actualParamCount =
WasmEdge_FunctionTypeGetParametersLength(functionType);
if (actualParamCount != expectedParamCount)
{
JLOG(j.trace()) << "HookSet(" << hook::log::FUNC_PARAM_INVALID
<< "): Import function '" << extName << "' has "
<< actualParamCount << " parameters, expected "
<< expectedParamCount;
return Unexpected(
"Import function '" + extName +
"' has incorrect parameter count");
}
// Validate each parameter type
if (actualParamCount > 0)
{
std::vector<WasmEdge_ValType> actualParams(actualParamCount);
WasmEdge_FunctionTypeGetParameters(
functionType, actualParams.data(), actualParamCount);
for (uint32_t p = 0; p < actualParamCount; p++)
{
uint8_t expectedParamType = (*expectedSig)[1 + p];
if (actualParams[p] != expectedParamType)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::FUNC_PARAM_INVALID
<< "): Import function '" << extName << "' parameter "
<< p << " has incorrect type. Expected "
<< static_cast<int>(expectedParamType) << ", found "
<< static_cast<int>(actualParams[p]);
return Unexpected(
"Import function '" + extName +
"' has incorrect parameter types");
}
}
}
}
return {};
}
Expected<bool, std::string>
validateWasmHostFunctionsForGas(
std::vector<uint8_t> const& wasm,
Rules const& rules,
beast::Journal const& j)
{
// Create WasmEdge Loader
WasmEdge_LoaderContext* loader = WasmEdge_LoaderCreate(NULL);
if (!loader)
{
return Unexpected("Failed to create WasmEdge Loader");
}
// Parse WASM binary
WasmEdge_ASTModuleContext* astModule = NULL;
WasmEdge_Result res = WasmEdge_LoaderParseFromBuffer(
loader, &astModule, wasm.data(), wasm.size());
if (!WasmEdge_ResultOK(res))
{
WasmEdge_LoaderDelete(loader);
const char* msg = WasmEdge_ResultGetMessage(res);
return Unexpected(
std::string("Failed to parse WASM: ") +
(msg ? msg : "unknown error"));
}
bool foundCbak = false;
//
// check export section
//
auto resultExport = validateExportSection(astModule, j);
if (!resultExport)
{
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return Unexpected(resultExport.error());
}
foundCbak = resultExport.value();
//
// check import section
//
if (auto result = validateImportSection(astModule, rules, j); !result)
{
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return Unexpected(result.error());
}
// Cleanup
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return foundCbak;
}
} // namespace hook

View File

@@ -1197,7 +1197,9 @@ hook::apply(
bool isStrong,
uint32_t wasmParam,
uint8_t hookChainPosition,
std::shared_ptr<STObject const> const& provisionalMeta)
std::shared_ptr<STObject const> const& provisionalMeta,
uint16_t hookApiVersion,
uint32_t hookGas)
{
HookContext hookCtx = {
.applyCtx = applyCtx,
@@ -1228,7 +1230,9 @@ hook::apply(
.wasmParam = wasmParam,
.hookChainPosition = hookChainPosition,
.foreignStateSetDisabled = false,
.provisionalMeta = provisionalMeta},
.provisionalMeta = provisionalMeta,
.hookApiVersion = hookApiVersion,
.hookGas = hookGas},
.emitFailure = isCallback && wasmParam & 1
? std::optional<ripple::STObject>(
(*(applyCtx.view().peek(keylet::emittedTxn(
@@ -1243,10 +1247,14 @@ hook::apply(
executor.executeWasm(
wasm.data(), (size_t)wasm.size(), isCallback, wasmParam, j);
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: "
<< (hookCtx.result.exitType == hook_api::ExitType::ROLLBACK
? "ROLLBACK"
: "ACCEPT")
auto const& exitType = hookCtx.result.exitType;
auto const& exitTypeStr = exitType == ExitType::ROLLBACK ? "ROLLBACK"
: exitType == ExitType::ACCEPT ? "ACCEPT"
: exitType == ExitType::GAS_INSUFFICIENT ? "GAS_INSUFFICIENT"
: exitType == ExitType::WASM_ERROR ? "WASM_ERROR"
: "UNSET";
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: " << exitTypeStr
<< " RS: '" << hookCtx.result.exitReason.c_str()
<< "' RC: " << hookCtx.result.exitCode;
@@ -1735,6 +1743,9 @@ hook::finalizeHookResult(
ripple::Slice{
hookResult.exitReason.data(), hookResult.exitReason.size()});
meta.setFieldU64(sfHookInstructionCount, hookResult.instructionCount);
if (hookResult.hookApiVersion == 1)
meta.setFieldU32(sfHookInstructionCost, hookResult.instructionCost);
meta.setFieldU16(
sfHookEmitCount,
emission_txnid.size()); // this will never wrap, hard limit

View File

@@ -244,6 +244,12 @@ Ledger::Ledger(
sle->at(sfReserveIncrement) = *f;
sle->at(sfReferenceFeeUnits) = Config::FEE_UNITS_DEPRECATED;
}
if (std::find(amendments.begin(), amendments.end(), featureHookGas) !=
amendments.end())
{
if (auto const f = config.FEES.hook_gas_price)
sle->at(sfHookGasPrice) = f;
}
rawInsert(sle);
}
@@ -695,12 +701,22 @@ Ledger::setup()
assign(fees_.increment, reserveIncrementXRP);
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
}
// Read GasPrice
bool hookFees = false;
if (auto const gp = sle->at(~sfHookGasPrice))
{
fees_.hookGasPrice = *gp;
hookFees = true;
}
if (oldFees && newFees)
// Should be all of one or the other, but not both
ret = false;
if (!rules_.enabled(featureXRPFees) && newFees)
// Can't populate the new fees before the amendment is enabled
ret = false;
if (!rules_.enabled(featureHookGas) && hookFees)
// Can't populate hook gas price before the amendment is enabled
ret = false;
}
}
catch (SHAMapMissingNode const&)
@@ -720,7 +736,8 @@ void
Ledger::defaultFees(Config const& config)
{
XRPL_ASSERT(
fees_.base == 0 && fees_.reserve == 0 && fees_.increment == 0,
fees_.base == 0 && fees_.reserve == 0 && fees_.increment == 0 &&
fees_.hookGasPrice == 0,
"ripple::Ledger::defaultFees : zero fees");
if (fees_.base == 0)
fees_.base = config.FEES.reference_fee;
@@ -728,6 +745,8 @@ Ledger::defaultFees(Config const& config)
fees_.reserve = config.FEES.account_reserve;
if (fees_.increment == 0)
fees_.increment = config.FEES.owner_reserve;
if (fees_.hookGasPrice == 0)
fees_.hookGasPrice = config.FEES.hook_gas_price;
}
std::shared_ptr<SLE>

View File

@@ -29,10 +29,10 @@ namespace ripple {
namespace detail {
template <typename value_type>
class VotableValue
{
private:
using value_type = XRPAmount;
value_type const current_; // The current setting
value_type const target_; // The setting we want
std::map<value_type, int> voteMap_;
@@ -67,8 +67,9 @@ public:
getVotes() const;
};
auto
VotableValue::getVotes() const -> std::pair<value_type, bool>
template <typename value_type>
std::pair<value_type, bool>
VotableValue<value_type>::getVotes() const
{
value_type ourVote = current_;
int weight = 0;
@@ -191,6 +192,18 @@ FeeVoteImpl::doValidation(
"reserve increment",
sfReserveIncrement);
}
// GasPrice voting (always uses UINT64, independent of featureXRPFees)
if (rules.enabled(featureHookGas))
{
if (lastFees.hookGasPrice != target_.hook_gas_price)
{
JLOG(journal_.info())
<< "Voting for hook gas price of " << target_.hook_gas_price;
if (auto const f = target_.hook_gas_price)
v[sfHookGasPrice] = f;
}
}
}
void
@@ -213,11 +226,14 @@ FeeVoteImpl::doVoting(
detail::VotableValue incReserveVote(
lastClosedLedger->fees().increment, target_.owner_reserve);
detail::VotableValue hookGasPriceVote(
lastClosedLedger->fees().hookGasPrice, target_.hook_gas_price);
auto const& rules = lastClosedLedger->rules();
if (rules.enabled(featureXRPFees))
{
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue& value,
detail::VotableValue<XRPAmount>& value,
SF_AMOUNT const& xrpField) {
if (auto const field = ~val->at(~xrpField);
field && field->native())
@@ -246,7 +262,7 @@ FeeVoteImpl::doVoting(
else
{
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue& value,
detail::VotableValue<XRPAmount>& value,
auto const& valueField) {
if (auto const field = val->at(~valueField))
{
@@ -278,6 +294,33 @@ FeeVoteImpl::doVoting(
}
}
// GasPrice voting (UINT64 field, independent of featureXRPFees)
if (rules.enabled(featureHookGas))
{
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue<std::uint64_t>& value,
SF_UINT64 const& xrpField) {
if (auto const field = val->at(~xrpField))
{
auto const vote = *field;
if (vote <= std::numeric_limits<std::uint64_t>::max())
value.addVote(vote);
else
value.noVote();
}
else
{
value.noVote();
}
};
for (auto const& val : set)
{
if (!val->isTrusted())
continue;
doVote(val, hookGasPriceVote, sfHookGasPrice);
}
}
// choose our positions
// TODO: Use structured binding once LLVM 16 is the minimum supported
// version. See also: https://github.com/llvm/llvm-project/issues/48582
@@ -285,11 +328,13 @@ FeeVoteImpl::doVoting(
auto const baseFee = baseFeeVote.getVotes();
auto const baseReserve = baseReserveVote.getVotes();
auto const incReserve = incReserveVote.getVotes();
auto const hookGasPrice = hookGasPriceVote.getVotes();
auto const seq = lastClosedLedger->info().seq + 1;
// add transactions to our position
if (baseFee.second || baseReserve.second || incReserve.second)
if (baseFee.second || baseReserve.second || incReserve.second ||
(rules.enabled(featureHookGas) && hookGasPrice.second))
{
JLOG(journal_.warn())
<< "We are voting for a fee change: " << baseFee.first << "/"
@@ -317,6 +362,10 @@ FeeVoteImpl::doVoting(
incReserveVote.current());
obj[sfReferenceFeeUnits] = Config::FEE_UNITS_DEPRECATED;
}
if (rules.enabled(featureHookGas))
{
obj[sfHookGasPrice] = hookGasPrice.first;
}
});
uint256 txID = feeTx.getTransactionID();

View File

@@ -152,6 +152,18 @@ Change::preclaim(PreclaimContext const& ctx)
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
return temDISABLED;
}
// sfHookGasPrice: required when featureHookGas is enabled,
// forbidden when disabled
if (ctx.view.rules().enabled(featureHookGas))
{
if (!ctx.tx.isFieldPresent(sfHookGasPrice))
return temMALFORMED;
}
else
{
if (ctx.tx.isFieldPresent(sfHookGasPrice))
return temDISABLED;
}
return tesSUCCESS;
case ttAMENDMENT:
case ttUNL_MODIFY:
@@ -639,7 +651,7 @@ Change::activateXahauGenesis()
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
wasmBytes.data(), (size_t)wasmBytes.size());
wasmBytes.data(), (size_t)wasmBytes.size(), 0);
if (result2)
{
@@ -1022,6 +1034,8 @@ Change::applyFee()
set(feeObject, ctx_.tx, sfBaseFeeDrops);
set(feeObject, ctx_.tx, sfReserveBaseDrops);
set(feeObject, ctx_.tx, sfReserveIncrementDrops);
if (ctx_.tx.isFieldPresent(sfHookGasPrice))
set(feeObject, ctx_.tx, sfHookGasPrice);
// Ensure the old fields are removed
feeObject->makeFieldAbsent(sfBaseFee);
feeObject->makeFieldAbsent(sfReferenceFeeUnits);

View File

@@ -26,6 +26,7 @@
#include <xrpld/ledger/ApplyView.h>
#include <xrpl/basics/Log.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/hook/GasValidator.h>
#include <xrpl/hook/Guard.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -230,7 +231,9 @@ SetHook::inferOperation(STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookOnIncoming))) &&
!hookSetObj.isFieldPresent(sfHookCanEmit) &&
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
!hookSetObj.isFieldPresent(sfFlags))
!hookSetObj.isFieldPresent(sfFlags) &&
!hookSetObj.isFieldPresent(sfHookCallbackGas) &&
!hookSetObj.isFieldPresent(sfHookWeakGas))
return hsoNOOP;
uint32_t flags = hookSetObj.isFieldPresent(sfFlags)
@@ -267,6 +270,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookOnIncoming) ||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
hookSetObj.isFieldPresent(sfHookCallbackGas) ||
hookSetObj.isFieldPresent(sfHookWeakGas) ||
!hookSetObj.isFieldPresent(sfFlags) ||
!hookSetObj.isFieldPresent(sfHookNamespace))
{
@@ -300,6 +305,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
hookSetObj.isFieldPresent(sfHookNamespace) ||
hookSetObj.isFieldPresent(sfHookCallbackGas) ||
hookSetObj.isFieldPresent(sfHookWeakGas) ||
!hookSetObj.isFieldPresent(sfFlags))
{
JLOG(ctx.j.trace())
@@ -402,6 +409,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// namespace may be valid, if the user so chooses
// hookon may be present if the user so chooses
// flags may be present if the user so chooses
// hookweakgas may be present if the user so chooses
// hookcallbackgas may be present if the user so chooses
return true;
}
@@ -441,7 +450,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
}
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
if (version != 0)
if (!ctx.rules.enabled(featureHookGas) && version != 0)
{
// we currently only accept api version 0
JLOG(ctx.j.trace())
@@ -451,6 +461,92 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return false;
}
// allow only version=0 and version=1
if (version != 0 && version != 1)
{
JLOG(ctx.j.trace())
<< "HookSet(" << ::hook::log::API_INVALID << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfHook->sfHookApiVersion invalid. (Must be 0 or 1).";
return false;
}
// validate sfHookCallbackGas
auto hasHookCallbackGas = false;
if (hookSetObj.isFieldPresent(sfHookCallbackGas))
{
if (version != 1)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfHookCallbackGas is "
"not allowed in version "
<< version << ".";
return false;
}
if (hookSetObj.getFieldU32(sfHookCallbackGas) == 0)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfHookCallbackGas must be greater than 0.";
return false;
}
hasHookCallbackGas = true;
}
// validate sfHookWeakGas
if (hookSetObj.isFieldPresent(sfHookWeakGas))
{
if (version != 1)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook sfHookWeakGas is "
"not allowed in version "
<< version << ".";
return false;
}
if (hookSetObj.getFieldU32(sfHookWeakGas) == 0)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook sfHookWeakGas "
"must be greater than 0.";
return false;
}
if (!(flags & hsfCOLLECT))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook with "
"sfHookWeakGas must be used with hsfCOLLECT "
"flag.";
return false;
}
}
else if (version == 1)
{
if (flags & hsfCOLLECT)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook with "
"hsfCOLLECT flag requires sfHookWeakGas to be "
"present.";
return false;
}
}
// validate sfHookOn
if (!hookSetObj.isFieldPresent(sfHookOn))
{
@@ -516,6 +612,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return {};
Blob hook = hookSetObj.getFieldVL(sfCreateCode);
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
// RH NOTE: validateGuards has a generic non-rippled specific
// interface so it can be used in other projects (i.e. tooling).
@@ -533,46 +630,97 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hsacc = ss.str();
}
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
hook_api::getImportWhitelist(ctx.rules),
hook_api::getGuardRulesVersion(ctx.rules));
uint64_t maxInstrCountHook = 0;
uint64_t maxInstrCountCbak = 0;
if (ctx.j.trace())
if (version == 0) // Guard type
{
// clunky but to get the stream to accept the output
// correctly we will split on new line and feed each line
// one by one into the trace stream beast::Journal should be
// updated to inherit from basic_ostream<char> then this
// wouldn't be necessary.
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
hook_api::getImportWhitelist(ctx.rules),
hook_api::getGuardRulesVersion(ctx.rules));
// is this a needless copy or does the compiler do copy
// elision here?
std::string s = loggerStream.str();
char* data = s.data();
size_t len = s.size();
char* last = data;
size_t i = 0;
for (; i < len; ++i)
if (ctx.j.trace())
{
if (data[i] == '\n')
// clunky but to get the stream to accept the output
// correctly we will split on new line and feed each
// line one by one into the trace stream beast::Journal
// should be updated to inherit from basic_ostream<char>
// then this wouldn't be necessary.
// is this a needless copy or does the compiler do copy
// elision here?
std::string s = loggerStream.str();
char* data = s.data();
size_t len = s.size();
char* last = data;
size_t i = 0;
for (; i < len; ++i)
{
data[i] = '\0';
ctx.j.trace() << last;
last = data + i;
if (data[i] == '\n')
{
data[i] = '\0';
ctx.j.trace() << last;
last = data + i;
}
}
if (last < data + i)
ctx.j.trace() << last;
}
if (last < data + i)
ctx.j.trace() << last;
}
if (!result)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_BAD_MAGIC << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfCreateCode failed validation.";
return false;
}
if (!result)
return false;
std::tie(maxInstrCountHook, maxInstrCountCbak) = *result;
}
else if (version == 1) // Gas type
{
// validate with GasValidator
auto validationResult =
hook::validateWasmHostFunctionsForGas(
hook, ctx.rules, ctx.j);
if (!validationResult)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::IMPORT_ILLEGAL << ")["
<< HS_ACC()
<< "]: Malformed transaction: Gas-type Hook "
"validation failed: "
<< validationResult.error();
return false;
}
auto const hasCbak = validationResult.value();
if ((!hasCbak && hasHookCallbackGas) ||
(hasCbak && !hasHookCallbackGas))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD
<< ")[" << HS_ACC()
<< "]: Malformed transaction: Gas-type Hook must "
"contain either sfHookCallbackGas if it "
"contains cbak function";
return false;
}
// Gas type: maxInstrCount is not pre-calculated (use Gas
// limit at runtime)
maxInstrCountHook = 0;
maxInstrCountCbak = 0;
}
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_SMOKE_TEST << ")["
@@ -582,7 +730,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
hook.data(), (size_t)hook.size());
hook.data(), (size_t)hook.size(), version);
if (result2)
{
@@ -594,7 +742,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return false;
}
return *result;
return std::make_pair(maxInstrCountHook, maxInstrCountCbak);
}
}
@@ -783,6 +931,11 @@ SetHook::preflight(PreflightContext const& ctx)
hookSetObj.isFieldPresent(sfHookCanEmit))
return temDISABLED;
if (!ctx.rules.enabled(featureHookGas) &&
(hookSetObj.isFieldPresent(sfHookCallbackGas) ||
hookSetObj.isFieldPresent(sfHookWeakGas)))
return temDISABLED;
for (auto const& hookSetElement : hookSetObj)
{
auto const& name = hookSetElement.getFName();
@@ -792,7 +945,8 @@ SetHook::preflight(PreflightContext const& ctx)
name != sfHookOn && name != sfHookOnOutgoing &&
name != sfHookOnIncoming && name != sfHookGrants &&
name != sfHookApiVersion && name != sfFlags &&
name != sfHookCanEmit)
name != sfHookCanEmit && name != sfHookCallbackGas &&
name != sfHookWeakGas)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
@@ -1255,6 +1409,46 @@ struct KeyletComparator
}
};
TER
validateGasHook(
STObject const& hook,
std::shared_ptr<STLedgerEntry> const& defSLE)
{
auto const version = defSLE->getFieldU16(sfHookApiVersion);
if (version == 1)
{
// Gas Hook
if (!defSLE->isFieldPresent(sfHookCallbackGas) &&
hook.isFieldPresent(sfHookCallbackGas))
return tecHOOK_INVALID;
auto const flags = hook.getFlags();
auto const hasCollectFlag = flags & hsfCOLLECT;
if (hasCollectFlag)
{
// ltHook or ltHookDefinition must have sfHookWeakGas
if (!hook.isFieldPresent(sfHookWeakGas) &&
!defSLE->isFieldPresent(sfHookWeakGas))
return tecHOOK_INVALID;
}
else
{
// ltHook must not have sfHookWeakGas
if (hook.isFieldPresent(sfHookWeakGas))
return tecHOOK_INVALID;
}
}
else
{
// Guard Hook
if (hook.isFieldPresent(sfHookCallbackGas) ||
hook.isFieldPresent(sfHookWeakGas))
return tecHOOK_INVALID;
}
return tesSUCCESS;
}
TER
SetHook::setHook()
{
@@ -1348,6 +1542,14 @@ SetHook::setHook()
std::optional<uint256> newHookCanEmit;
std::optional<uint256> defHookCanEmit;
std::optional<uint32_t> oldHookWeakGas;
std::optional<uint32_t> newHookWeakGas;
std::optional<uint32_t> defHookWeakGas;
std::optional<uint32_t> oldHookCallbackGas;
std::optional<uint32_t> newHookCallbackGas;
std::optional<uint32_t> defHookCallbackGas;
// when hsoCREATE is invoked it populates this variable in case the hook
// definition already exists and the operation falls through into a
// hsoINSTALL operation instead
@@ -1421,6 +1623,23 @@ SetHook::setHook()
oldHookCanEmit = oldHook->get().getFieldH256(sfHookCanEmit);
else if (defHookCanEmit)
oldHookCanEmit = *defHookCanEmit;
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookWeakGas))
defHookWeakGas = oldDefSLE->getFieldU32(sfHookWeakGas);
if (oldHook && oldHook->get().isFieldPresent(sfHookWeakGas))
oldHookWeakGas = oldHook->get().getFieldU32(sfHookWeakGas);
else if (defHookWeakGas)
oldHookWeakGas = *defHookWeakGas;
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookCallbackGas))
defHookCallbackGas = oldDefSLE->getFieldU32(sfHookCallbackGas);
if (oldHook && oldHook->get().isFieldPresent(sfHookCallbackGas))
oldHookCallbackGas =
oldHook->get().getFieldU32(sfHookCallbackGas);
else if (defHookCallbackGas)
oldHookCallbackGas = *defHookCallbackGas;
}
// in preparation for three way merge populate fields if they are
@@ -1453,6 +1672,13 @@ SetHook::setHook()
newNamespace = hookSetObj->get().getFieldH256(sfHookNamespace);
newDirKeylet = keylet::hookStateDir(account_, *newNamespace);
}
if (hookSetObj->get().isFieldPresent(sfHookCallbackGas))
newHookCallbackGas =
hookSetObj->get().getFieldU32(sfHookCallbackGas);
if (hookSetObj->get().isFieldPresent(sfHookWeakGas))
newHookWeakGas = hookSetObj->get().getFieldU32(sfHookWeakGas);
}
// users may destroy a namespace in any operation except NOOP and
@@ -1638,6 +1864,40 @@ SetHook::setHook()
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
}
auto const defVersion =
oldDefSLE->getFieldU16(sfHookApiVersion);
if (defVersion == 0 && (newHookCallbackGas || newHookWeakGas))
return tecHOOK_INVALID;
if (!defHookCallbackGas.has_value() && newHookCallbackGas)
return tecHOOK_INVALID;
if (newHookWeakGas)
{
if (defHookWeakGas.has_value() &&
*defHookWeakGas == *newHookWeakGas)
{
if (newHook.isFieldPresent(sfHookWeakGas))
newHook.makeFieldAbsent(sfHookWeakGas);
}
else
newHook.setFieldU32(sfHookWeakGas, *newHookWeakGas);
}
if (newHookCallbackGas)
{
if (defHookCallbackGas.has_value() &&
*defHookCallbackGas == *newHookCallbackGas)
{
if (newHook.isFieldPresent(sfHookCallbackGas))
newHook.makeFieldAbsent(sfHookCallbackGas);
}
else
newHook.setFieldU32(
sfHookCallbackGas, *newHookCallbackGas);
}
// parameters
if (hookSetObj->get().isFieldPresent(sfHookParameters) &&
hookSetObj->get().getFieldArray(sfHookParameters).empty())
@@ -1682,6 +1942,10 @@ SetHook::setHook()
if (flags)
newHook.setFieldU32(sfFlags, *flags);
TER result = validateGasHook(newHook, oldDefSLE);
if (!isTesSuccess(result))
return result;
newHooks.push_back(std::move(newHook));
continue;
}
@@ -1816,10 +2080,24 @@ SetHook::setHook()
newHookDef->setFieldH256(
sfHookSetTxnID, ctx.tx.getTransactionID());
newHookDef->setFieldU64(sfReferenceCount, 1);
newHookDef->setFieldAmount(
sfFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountHook)});
// Set HookCallbackGas if present in hookSetObj
if (hookSetObj->get().isFieldPresent(sfHookCallbackGas))
newHookDef->setFieldU32(
sfHookCallbackGas,
hookSetObj->get().getFieldU32(sfHookCallbackGas));
// Set HookWeakGas if present in hookSetObj
if (hookSetObj->get().isFieldPresent(sfHookWeakGas))
newHookDef->setFieldU32(
sfHookWeakGas,
hookSetObj->get().getFieldU32(sfHookWeakGas));
if (hookSetObj->get().getFieldU16(sfHookApiVersion) != 1)
newHookDef->setFieldAmount(
sfFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountHook)});
if (maxInstrCountCbak > 0)
newHookDef->setFieldAmount(
sfHookCallbackFee,
@@ -1841,6 +2119,38 @@ SetHook::setHook()
slesToInsert.emplace(keylet, newHookDef);
newHook.setFieldH256(sfHookHash, *createHookHash);
// Set HookCallbackGas in Hook object only if different from
// Definition
if (hookSetObj->get().isFieldPresent(sfHookCallbackGas))
{
uint32_t objGas =
hookSetObj->get().getFieldU32(sfHookCallbackGas);
if (!newHookDef->isFieldPresent(sfHookCallbackGas) ||
newHookDef->getFieldU32(sfHookCallbackGas) !=
objGas)
{
newHook.setFieldU32(sfHookCallbackGas, objGas);
}
}
// Set HookWeakGas in Hook object only if different from
// Definition
if (hookSetObj->get().isFieldPresent(sfHookWeakGas))
{
uint32_t objGas =
hookSetObj->get().getFieldU32(sfHookWeakGas);
if (!newHookDef->isFieldPresent(sfHookWeakGas) ||
newHookDef->getFieldU32(sfHookWeakGas) != objGas)
{
newHook.setFieldU32(sfHookWeakGas, objGas);
}
}
TER result = validateGasHook(newHook, newHookDef);
if (!isTesSuccess(result))
return result;
newHooks.push_back(std::move(newHook));
continue;
}
@@ -1912,6 +2222,14 @@ SetHook::setHook()
defHookOnOutgoing =
newDefSLE->getFieldH256(sfHookOnOutgoing);
// refresh gas fields from the new definition
if (newDefSLE->isFieldPresent(sfHookCallbackGas))
defHookCallbackGas =
newDefSLE->getFieldU32(sfHookCallbackGas);
if (newDefSLE->isFieldPresent(sfHookWeakGas))
defHookWeakGas = newDefSLE->getFieldU32(sfHookWeakGas);
// set the hookon field if it differs from definition
if (newHookOn)
{
@@ -1950,6 +2268,28 @@ SetHook::setHook()
*defHookCanEmit == *newHookCanEmit))
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
auto const defVersion =
newDefSLE->getFieldU16(sfHookApiVersion);
if (defVersion == 0 && (newHookCallbackGas || newHookWeakGas))
return tecHOOK_INVALID;
if (!defHookCallbackGas.has_value() && newHookCallbackGas)
return tecHOOK_INVALID;
if (newHookCallbackGas &&
!(defHookCallbackGas.has_value() &&
*defHookCallbackGas == *newHookCallbackGas))
newHook.setFieldU32(sfHookCallbackGas, *newHookCallbackGas);
if ((!flags || !(*flags & hsfCOLLECT)) && newHookWeakGas)
return tecHOOK_INVALID;
if (newHookWeakGas &&
!(defHookWeakGas.has_value() &&
*defHookWeakGas == *newHookWeakGas))
newHook.setFieldU32(sfHookWeakGas, *newHookWeakGas);
// parameters
TER result = updateHookParameters(
ctx,
@@ -1973,6 +2313,10 @@ SetHook::setHook()
if (flags)
newHook.setFieldU32(sfFlags, newFlags);
result = validateGasHook(newHook, newDefSLE);
if (!isTesSuccess(result))
return result;
newHooks.push_back(std::move(newHook));
slesToUpdate.emplace(*newDefKeylet, newDefSLE);
@@ -1990,6 +2334,8 @@ SetHook::setHook()
}
}
JLOG(ctx.j.warn()) << "HookSet: setHook after for loops";
int reserveDelta = 0;
{
// compute owner counts before modifying anything on ledger
@@ -1999,7 +2345,8 @@ SetHook::setHook()
// sfParameters: 1 reserve PER entry
// sfGrants are: 1 reserve PER entry
// sfHookHash, sfHookNamespace, sfHookOn, sfHookOnOutgoing,
// sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags: free
// sfHookOnIncoming, sfHookCanEmit, sfHookApiVersion, sfHookCallbackGas,
// sfHookWeakGas, sfFlags: free
// sfHookDefinition is not reserved because it is an unowned object,
// rather the uploader is billed via fee according to the following:
@@ -2146,6 +2493,7 @@ SetHook::setHook()
view().update(accountSLE);
}
JLOG(ctx.j.warn()) << "HookSet: setHook end";
return nsDeleteResult;
} // namespace ripple

View File

@@ -101,6 +101,11 @@ preflight1(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfHookGas) && !ctx.rules.enabled(featureHookGas))
{
return temMALFORMED;
}
auto const ret = preflight0(ctx);
if (!isTesSuccess(ret))
return ret;
@@ -234,6 +239,21 @@ Transactor::Transactor(ApplyContext& ctx)
{
}
static constexpr uint64_t GAS_PRICE_MICRO_DROPS = 1'000'000;
XRPAmount
Transactor::calculateHookGas(uint32_t gasCount, Fees const& fees)
{
uint64_t const gasPrice =
fees.hookGasPrice > 0 ? fees.hookGasPrice : GAS_PRICE_MICRO_DROPS;
uint64_t gasCountU64 = static_cast<uint64_t>(gasCount);
if (gasPrice > 0 &&
gasCountU64 > std::numeric_limits<uint64_t>::max() / gasPrice)
return XRPAmount{INITIAL_XRP.drops()};
return XRPAmount{static_cast<XRPAmount::value_type>(
(gasCountU64 * gasPrice) / GAS_PRICE_MICRO_DROPS)};
}
// RH NOTE: this only computes one chain at a time, so if there is a receiving
// side to a txn then it must seperately be computed by a second call here
XRPAmount
@@ -249,6 +269,7 @@ Transactor::calculateHookChainFee(
return XRPAmount{0};
XRPAmount fee{0};
uint32_t gasTypeHookCount = 0; // Gas type hook counter
auto const& hooks = hookSLE->getFieldArray(sfHooks);
@@ -283,18 +304,57 @@ Transactor::calculateHookChainFee(
if (hook::canHook(tx.getTxnType(), hookOn) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
XRPAmount const toAdd{hookDef->getFieldAmount(sfFee).xrp().drops()};
// get HookApiVersion
uint16_t apiVersion = hookDef->getFieldU16(sfHookApiVersion);
// this overflow should never happen, if somehow it does
// fee is set to the largest possible valid xrp value to force
// fail the transaction
if (fee + toAdd < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
if (apiVersion == 0) // Guard type
{
// existing logic: read HookDefinition's sfFee
XRPAmount const toAdd{
hookDef->getFieldAmount(sfFee).xrp().drops()};
// this overflow should never happen, if somehow it does
// fee is set to the largest possible valid xrp value to force
// fail the transaction
if (fee + toAdd < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
}
else if (apiVersion == 1) // Gas type
{
if (!collectCallsOnly)
{
// Gas type: only count
gasTypeHookCount++;
}
else
{
auto const weakFee = hookObj.isFieldPresent(sfHookWeakGas)
? hookObj.getFieldU32(sfHookWeakGas)
: hookDef->getFieldU32(sfHookWeakGas);
XRPAmount const toAdd =
calculateHookGas(weakFee, view.fees());
if (fee + toAdd < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
}
}
}
}
// Additional cost for Gas type: baseFee * 100 /Hook = 10*100 drops/Hook
if (gasTypeHookCount > 0)
{
auto const baseGasFee = view.fees().base * 100;
XRPAmount const gasTypeFee{gasTypeHookCount * baseGasFee};
if (fee + gasTypeFee < fee)
fee = XRPAmount{INITIAL_XRP.drops()}; // overflow
else
fee += gasTypeFee;
}
return fee;
}
@@ -357,6 +417,51 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
hookExecutionFee += toAdd;
}
if (const auto hookSLE =
view.read(keylet::hook(tx.getAccountID(sfAccount))))
{
const auto& hooks = hookSLE->getFieldArray(sfHooks);
for (auto const& hookObj : hooks)
{
if (hookObj.isFieldPresent(sfHookHash))
{
if (hookObj.getFieldH256(sfHookHash) !=
callbackHookHash)
continue;
uint32_t callbackGas = 0;
// Priority 1: Check HookObject
if (hookObj.isFieldPresent(sfHookCallbackGas))
{
callbackGas =
hookObj.getFieldU32(sfHookCallbackGas);
}
// Priority 2: Check HookDefinition
else if (
hookDef &&
hookDef->isFieldPresent(sfHookCallbackGas))
{
callbackGas =
hookDef->getFieldU32(sfHookCallbackGas);
}
// Priority 3: Default to 0 (implicit)
if (callbackGas > 0)
{
XRPAmount const toAdd =
calculateHookGas(callbackGas, view.fees());
if (hookExecutionFee + toAdd < hookExecutionFee)
hookExecutionFee =
XRPAmount{INITIAL_XRP.drops()};
else
hookExecutionFee += toAdd;
}
break;
}
}
}
XRPL_ASSERT(
emitDetails.isFieldPresent(sfEmitBurden),
"Transactor::calculateBaseFee : emit burden not present");
@@ -376,6 +481,11 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
if (canRollback)
hookExecutionFee += calculateHookChainFee(
view, tx, keylet::hook(tshAcc), false);
if (view.rules().enabled(featureHookGas) &&
tx.isFieldPresent(sfHookGas))
hookExecutionFee +=
calculateHookGas(tx.getFieldU32(sfHookGas), view.fees());
}
XRPAmount accumulator = baseFee;
@@ -1290,7 +1400,8 @@ Transactor::executeHookChain(
ripple::AccountID const& account,
bool strong,
bool isOutgoing,
std::shared_ptr<STObject const> const& provisionalMeta)
std::shared_ptr<STObject const> const& provisionalMeta,
uint32_t& gasPool)
{
std::set<uint256> hookSkips;
std::map<uint256, std::map<std::vector<uint8_t>, std::vector<uint8_t>>>
@@ -1360,7 +1471,30 @@ Transactor::executeHookChain(
return tecINTERNAL;
}
bool hasCallback = hookDef->isFieldPresent(sfHookCallbackFee);
bool hasCallback = hookDef->isFieldPresent(sfHookCallbackFee) ||
hookDef->isFieldPresent(sfHookCallbackGas);
// Extract HookApiVersion for Gas-type hooks
uint16_t hookApiVersion = hookDef->isFieldPresent(sfHookApiVersion)
? hookDef->getFieldU16(sfHookApiVersion)
: 0;
// Prepare Gas limit for this hook execution
uint32_t hookGas = 0;
if (hookApiVersion == 1)
{
if (!strong) // WeakTSH execution
{
hookGas = hookObj.isFieldPresent(sfHookWeakGas)
? hookObj.getFieldU32(sfHookWeakGas)
: hookDef->getFieldU32(sfHookWeakGas);
}
else // Strong execution
{
// Pass remaining Gas pool to this hook
hookGas = gasPool;
}
}
try
{
@@ -1380,12 +1514,38 @@ Transactor::executeHookChain(
strong,
(strong ? 0 : 1UL), // 0 = strong, 1 = weak
hook_no - 1,
provisionalMeta));
provisionalMeta,
hookApiVersion,
hookGas));
executedHookCount_++;
hook::HookResult& hookResult = results.back();
// Track Gas consumption for Gas-type hooks
if (hookApiVersion == 1)
{
uint64_t consumed = hookResult.instructionCost;
JLOG(j_.trace()) << "HookChain: Hook consumed " << consumed
<< " instructions. Pool before: " << gasPool;
if (consumed >= gasPool)
{
JLOG(j_.trace()) << "HookError: Gas pool exhausted. "
<< "Hook tried to consume " << consumed
<< " but only " << gasPool << " remained.";
return tecHOOK_INSUFFICIENT_GAS;
}
gasPool -= consumed;
JLOG(j_.trace()) << "HookChain: Pool after: " << gasPool;
}
if (hookResult.exitType == hook_api::ExitType::GAS_INSUFFICIENT)
return tecHOOK_INSUFFICIENT_GAS;
if (hookResult.exitType != hook_api::ExitType::ACCEPT)
{
if (results.back().exitType == hook_api::ExitType::WASM_ERROR)
@@ -1465,7 +1625,8 @@ Transactor::doHookCallback(
return;
}
if (!hookDef->isFieldPresent(sfHookCallbackFee))
if (!hookDef->isFieldPresent(sfHookCallbackFee) &&
!hookDef->isFieldPresent(sfHookCallbackGas))
{
JLOG(j_.trace()) << "HookInfo[" << callbackAccountID
<< "]: Callback specified by emitted txn "
@@ -1522,6 +1683,24 @@ Transactor::doHookCallback(
{
hook::HookStateMap stateMap;
// Extract HookApiVersion for callback
uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion);
// Get callback gas with fallback priority:
// 1. HookObject's HookCallbackGas
// 2. HookDefinition's HookCallbackGas
// 3. Transaction's HookGas (for backward compatibility)
uint32_t hookGas = 0;
if (hookObj.isFieldPresent(sfHookCallbackGas))
{
hookGas = hookObj.getFieldU32(sfHookCallbackGas);
}
else if (hookDef->isFieldPresent(sfHookCallbackGas))
{
hookGas = hookDef->getFieldU32(sfHookCallbackGas);
}
hook::HookResult callbackResult = hook::apply(
hookDef->getFieldH256(sfHookSetTxnID),
callbackHookHash,
@@ -1541,7 +1720,9 @@ Transactor::doHookCallback(
? 1UL
: 0UL,
hook_no - 1,
provisionalMeta);
provisionalMeta,
hookApiVersion,
hookGas);
executedHookCount_++;
@@ -1609,7 +1790,8 @@ Transactor::doTSH(
std::vector<std::pair<AccountID, bool>> tsh,
hook::HookStateMap& stateMap,
std::vector<hook::HookResult>& results,
std::shared_ptr<STObject const> const& provisionalMeta)
std::shared_ptr<STObject const> const& provisionalMeta,
uint32_t& gasPool)
{
auto& view = ctx_.view();
@@ -1759,7 +1941,8 @@ Transactor::doTSH(
tshAccountID,
strong,
false,
provisionalMeta);
provisionalMeta,
gasPool);
if (canRollback && (!isTesSuccess(tshResult)))
return tshResult;
@@ -1830,6 +2013,19 @@ Transactor::doAgainAsWeak(
return;
}
// Extract HookApiVersion for aaw execution
uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion);
// Extract HookGas for Gas-type hooks
uint32_t hookGasWeak = 0;
if (hookApiVersion == 1)
{
if (hookObj.isFieldPresent(sfHookWeakGas))
hookGasWeak = hookObj.getFieldU32(sfHookWeakGas);
else if (hookDef->isFieldPresent(sfHookWeakGas))
hookGasWeak = hookDef->getFieldU32(sfHookWeakGas);
}
try
{
hook::HookResult aawResult = hook::apply(
@@ -1843,12 +2039,15 @@ Transactor::doAgainAsWeak(
stateMap,
ctx_,
hookAccountID,
hookDef->isFieldPresent(sfHookCallbackFee),
hookDef->isFieldPresent(sfHookCallbackFee) ||
hookDef->isFieldPresent(sfHookCallbackGas),
false,
false,
2UL, // param 2 = aaw
hook_no - 1,
provisionalMeta);
provisionalMeta,
hookApiVersion,
hookGasWeak);
executedHookCount_++;
@@ -1946,6 +2145,15 @@ Transactor::operator()()
auto const& accountID = ctx_.tx.getAccountID(sfAccount);
std::vector<hook::HookResult> hookResults;
// Initialize Gas pool once for strong TSH hook chains
uint32_t gasPool = 0;
if (ctx_.tx.isFieldPresent(sfHookGas))
{
gasPool = ctx_.tx.getFieldU32(sfHookGas);
JLOG(j_.trace()) << "HookChain: Initialized Gas pool with "
<< gasPool << " instructions";
}
auto const& hooksOriginator = view().read(keylet::hook(accountID));
// First check if the Sending account has any hooks that can be fired
@@ -1958,7 +2166,8 @@ Transactor::operator()()
accountID,
true,
true,
{});
{},
gasPool);
if (isTesSuccess(result))
{
@@ -1967,7 +2176,7 @@ Transactor::operator()()
// (who have the right to rollback the txn), any weak TSH will be
// executed after doApply has been successful (callback as well)
result = doTSH(true, tsh, stateMap, hookResults, {});
result = doTSH(true, tsh, stateMap, hookResults, {}, gasPool);
}
// write state if all chains executed successfully
@@ -2263,7 +2472,8 @@ Transactor::operator()()
tsh = hook::getTransactionalStakeHolders(ctx_.tx, ctx_.view());
}
doTSH(false, tsh, stateMap, weakResults, proMeta);
uint32_t weakGasPool = 0;
doTSH(false, tsh, stateMap, weakResults, proMeta, weakGasPool);
// execute any hooks that nominated for 'again as weak'
for (auto const& [accID, hookHashes] : aawMap)

View File

@@ -174,6 +174,9 @@ public:
// Hooks
static XRPAmount
calculateHookGas(uint32_t gasCount, Fees const& fees);
static XRPAmount
calculateHookChainFee(
ReadView const& view,
@@ -192,7 +195,8 @@ protected:
std::vector<std::pair<AccountID, bool>> tsh,
hook::HookStateMap& stateMap,
std::vector<hook::HookResult>& result,
std::shared_ptr<STObject const> const& provisionalMeta);
std::shared_ptr<STObject const> const& provisionalMeta,
uint32_t& gasPool);
// Execute a hook "Again As Weak" is a feature that allows
// a hook that which is being executed pre-application of the otxn
@@ -213,7 +217,8 @@ protected:
ripple::AccountID const& account,
bool strong,
bool isOutgoing,
std::shared_ptr<STObject const> const& provisionalMeta);
std::shared_ptr<STObject const> const& provisionalMeta,
uint32_t& gasPool);
void
addWeakTSHFromBalanceChanges(detail::ApplyViewBase const& pv);

View File

@@ -82,6 +82,9 @@ struct FeeSetup
/** The per-owned item reserve requirement in drops. */
XRPAmount owner_reserve{2 * DROPS_PER_XRP};
/** The gas price in micro-drops per gas unit. */
std::uint64_t hook_gas_price{1'000'000};
/* (Remember to update the example cfg files when changing any of these
* values.) */
};

View File

@@ -34,6 +34,7 @@
#include <boost/regex.hpp>
#include <boost/system/error_code.hpp>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <iterator>
@@ -1143,6 +1144,11 @@ setup_FeeVote(Section const& section)
if (set(temp, "owner_reserve", section))
setup.owner_reserve = temp;
}
{
std::int64_t temp;
if (set(temp, "hook_gas_price", section))
setup.hook_gas_price = temp;
}
return setup;
}
} // namespace ripple