Compare commits

..

4 Commits

Author SHA1 Message Date
Niq Dudfield
c4367eae0c Merge branch 'dev' into fix-ips-fixed 2025-12-24 08:21:02 +07:00
Niq Dudfield
e04464df38 Merge branch 'dev' into fix-ips-fixed 2025-11-29 04:29:13 +07:00
Niq Dudfield
f3c69ae34e Merge branch 'dev' into fix-ips-fixed 2025-11-27 19:46:22 +07:00
Nicholas Dudfield
aee8087042 fix: restore [ips_fixed] to use addFixedPeer instead of addFallbackStrings
Commit c65a0332bc refactored IP handling but accidentally broke [ips_fixed]
by treating fixed peers as regular bootstrap sources. This caused fixed
peers to not bypass slot limits, breaking connectivity for nodes with low
peers_max values.

This fix does NOT restore:
- The fallback behavior where [ips_fixed] was used as bootstrap when [ips]
  was empty (added in 98bdb9de68, 2015)
- Any hardcoded default bootstrap IPs

These are unnecessary because addFixedPeer actively maintains connections
to fixed peers regardless of the bootstrap mechanism. Nodes with only
[ips_fixed] configured will still connect and discover peers through
their fixed connections.

Closes #636
2025-11-26 09:29:26 +07:00
23 changed files with 96 additions and 3672 deletions

View File

@@ -43,9 +43,8 @@ jobs:
# To isolate environments for each Runner, instead of installing globally with brew,
# use mise to isolate environments for each Runner directory.
- name: Setup toolchain (mise)
uses: jdx/mise-action@v3.6.1
uses: jdx/mise-action@v2
with:
cache: false
install: true
- name: Install tools via mise

View File

@@ -488,7 +488,6 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/apply.cpp
src/ripple/app/tx/impl/applySteps.cpp
src/ripple/app/hook/impl/applyHook.cpp
src/ripple/app/hook/impl/GasValidator.cpp
src/ripple/app/tx/impl/details/NFTokenUtils.cpp
#[===============================[
main sources:
@@ -910,7 +909,6 @@ if (tests)
src/test/jtx/impl/fee.cpp
src/test/jtx/impl/flags.cpp
src/test/jtx/impl/genesis.cpp
src/test/jtx/impl/hookgas.cpp
src/test/jtx/impl/import.cpp
src/test/jtx/impl/invoice_id.cpp
src/test/jtx/impl/invoke.cpp

View File

@@ -63,8 +63,6 @@
#define sfEmitGeneration ((2U << 16U) + 46U)
#define sfLockCount ((2U << 16U) + 49U)
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
#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)

View File

@@ -1,57 +0,0 @@
//------------------------------------------------------------------------------
/*
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 <ripple/beast/utility/Journal.h>
#include <ripple/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 std::nullopt if validation succeeds, error message otherwise
*/
std::optional<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

@@ -10,6 +10,7 @@
#include <ripple/protocol/SField.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/digest.h>
#include <any>
#include <memory>
#include <optional>
#include <queue>
@@ -461,9 +462,7 @@ apply(
uint32_t wasmParam,
uint8_t hookChainPosition,
// result of apply() if this is weak exec
std::shared_ptr<STObject const> const& provisionalMeta,
uint16_t hookApiVersion,
uint32_t hookGas);
std::shared_ptr<STObject const> const& provisionalMeta);
struct HookContext;
@@ -502,7 +501,6 @@ 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
@@ -515,8 +513,6 @@ 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;
@@ -656,18 +652,12 @@ public:
WasmEdge_ConfigureContext* conf = NULL;
WasmEdge_VMContext* ctx = NULL;
WasmEdgeVM(uint16_t hookApiVersion)
WasmEdgeVM()
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
if (hookApiVersion == 1)
{
WasmEdge_ConfigureStatisticsSetCostMeasuring(conf, true);
uint32_t maxMemoryPage = 8;
WasmEdge_ConfigureSetMaxMemoryPage(conf, maxMemoryPage);
}
ctx = WasmEdge_VMCreate(conf, NULL);
}
@@ -702,9 +692,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, uint16_t hookApiVersion)
validateWasm(const void* wasm, size_t len)
{
WasmEdgeVM vm{hookApiVersion};
WasmEdgeVM vm;
if (!vm.sane())
return "Could not create WASMEDGE instance";
@@ -747,7 +737,7 @@ public:
WasmEdge_LogOff();
WasmEdgeVM vm{hookCtx.result.hookApiVersion};
WasmEdgeVM vm;
if (!vm.sane())
{
@@ -768,22 +758,6 @@ 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];
@@ -797,29 +771,17 @@ public:
returns,
1);
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;
}
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);
// RH NOTE: stack unwind will clean up WasmEdgeVM
}

View File

@@ -1,393 +0,0 @@
//------------------------------------------------------------------------------
/*
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 <ripple/app/hook/GasValidator.h>
#include <ripple/app/hook/Guard.h>
#include <ripple/app/hook/Macro.h>
#include <ripple/basics/Expected.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Feature.h>
#include <wasmedge/wasmedge.h>
namespace hook {
Expected<void, 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;
// Check each export
for (uint32_t i = 0; i < actualExportCount; i++)
{
// Only check function exports
WasmEdge_ExternalType const type =
WasmEdge_ExportTypeGetExternalType(exports[i]);
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;
// 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 {};
}
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]);
// Only check function imports
if (extType != WasmEdge_ExternalType_Function)
continue;
// 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 baseIt = hook_api::import_whitelist.find(extName);
if (baseIt != hook_api::import_whitelist.end())
{
expectedSig = &baseIt->second;
}
else if (rules.enabled(featureHooksUpdate1))
{
auto extIt = hook_api::import_whitelist_1.find(extName);
if (extIt != hook_api::import_whitelist_1.end())
{
expectedSig = &extIt->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 {};
}
std::optional<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 "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 std::string("Failed to parse WASM: ") +
(msg ? msg : "unknown error");
}
//
// check export section
//
if (auto result = validateExportSection(astModule, j); !result)
{
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return result.error();
}
//
// check import section
//
if (auto result = validateImportSection(astModule, rules, j); !result)
{
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return result.error();
}
// Cleanup
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return {};
}
} // namespace hook

View File

@@ -1232,9 +1232,7 @@ hook::apply(
bool isStrong,
uint32_t wasmParam,
uint8_t hookChainPosition,
std::shared_ptr<STObject const> const& provisionalMeta,
uint16_t hookApiVersion,
uint32_t hookGas)
std::shared_ptr<STObject const> const& provisionalMeta)
{
HookContext hookCtx = {
.applyCtx = applyCtx,
@@ -1266,9 +1264,7 @@ hook::apply(
.wasmParam = wasmParam,
.hookChainPosition = hookChainPosition,
.foreignStateSetDisabled = false,
.provisionalMeta = provisionalMeta,
.hookApiVersion = hookApiVersion,
.hookGas = hookGas},
.provisionalMeta = provisionalMeta},
.emitFailure = isCallback && wasmParam & 1
? std::optional<ripple::STObject>(
(*(applyCtx.view().peek(keylet::emittedTxn(
@@ -2053,9 +2049,6 @@ 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

@@ -638,7 +638,7 @@ Change::activateXahauGenesis()
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
wasmBytes.data(), (size_t)wasmBytes.size(), 0);
wasmBytes.data(), (size_t)wasmBytes.size());
if (result2)
{

View File

@@ -20,7 +20,6 @@
#include <ripple/app/tx/impl/SetHook.h>
#include <ripple/app/hook/Enum.h>
#include <ripple/app/hook/GasValidator.h>
#include <ripple/app/hook/Guard.h>
#include <ripple/app/hook/applyHook.h>
#include <ripple/app/ledger/Ledger.h>
@@ -436,8 +435,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
}
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
if (!ctx.rules.enabled(featureHookGas) && version != 0)
if (version != 0)
{
// we currently only accept api version 0
JLOG(ctx.j.trace())
@@ -447,17 +445,6 @@ 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 sfHookOn
if (!hookSetObj.isFieldPresent(sfHookOn))
{
@@ -482,7 +469,6 @@ 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).
@@ -500,83 +486,46 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hsacc = ss.str();
}
uint64_t maxInstrCountHook = 0;
uint64_t maxInstrCountCbak = 0;
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
(ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) +
(ctx.rules.enabled(fix20250131) ? 2 : 0));
if (version == 0) // Guard type
if (ctx.j.trace())
{
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
(ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) +
(ctx.rules.enabled(fix20250131) ? 2 : 0));
// 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.
if (ctx.j.trace())
// 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)
{
// 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)
if (data[i] == '\n')
{
if (data[i] == '\n')
{
data[i] = '\0';
ctx.j.trace() << last;
last = data + i;
}
}
if (last < data + i)
data[i] = '\0';
ctx.j.trace() << last;
last = data + i;
}
}
if (!result)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_BAD_MAGIC << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfCreateCode failed validation.";
return false;
}
std::tie(maxInstrCountHook, maxInstrCountCbak) = *result;
if (last < data + i)
ctx.j.trace() << last;
}
else if (version == 1) // Gas type
{
// validate with GasValidator
auto error = hook::validateWasmHostFunctionsForGas(
hook, ctx.rules, ctx.j);
if (error)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::IMPORT_ILLEGAL << ")["
<< HS_ACC()
<< "]: Malformed transaction: Gas-type Hook "
"validation failed: "
<< *error;
return false;
}
// Gas type: maxInstrCount is not pre-calculated (use Gas
// limit at runtime)
maxInstrCountHook = 0;
maxInstrCountCbak = 0;
}
if (!result)
return false;
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_SMOKE_TEST << ")["
@@ -586,7 +535,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
hook.data(), (size_t)hook.size(), version);
hook.data(), (size_t)hook.size());
if (result2)
{
@@ -598,7 +547,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return false;
}
return std::make_pair(maxInstrCountHook, maxInstrCountCbak);
return *result;
}
}
@@ -1731,11 +1680,10 @@ SetHook::setHook()
newHookDef->setFieldH256(
sfHookSetTxnID, ctx.tx.getTransactionID());
newHookDef->setFieldU64(sfReferenceCount, 1);
if (hookSetObj->get().getFieldU16(sfHookApiVersion) != 1)
newHookDef->setFieldAmount(
sfFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountHook)});
newHookDef->setFieldAmount(
sfFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountHook)});
if (maxInstrCountCbak > 0)
newHookDef->setFieldAmount(
sfHookCallbackFee,

View File

@@ -100,11 +100,6 @@ 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;
@@ -224,7 +219,6 @@ Transactor::calculateHookChainFee(
return XRPAmount{0};
XRPAmount fee{0};
uint32_t gasTypeHookCount = 0; // Gas type hook counter
auto const& hooks = hookSLE->getFieldArray(sfHooks);
@@ -261,43 +255,18 @@ Transactor::calculateHookChainFee(
if (hook::canHook(tx.getTxnType(), hookOn) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
// get HookApiVersion
uint16_t apiVersion = hookDef->getFieldU16(sfHookApiVersion);
XRPAmount const toAdd{hookDef->getFieldAmount(sfFee).xrp().drops()};
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
{
// Gas type: only count
gasTypeHookCount++;
}
// 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;
}
}
// Additional cost for Gas type: 0.2 XAH/Hook = 200,000 drops/Hook
if (gasTypeHookCount > 0)
{
// TODO:
auto const baseGasFee = 200000;
XRPAmount const gasTypeFee{gasTypeHookCount * baseGasFee};
if (fee + gasTypeFee < fee)
fee = XRPAmount{INITIAL_XRP.drops()}; // overflow
else
fee += gasTypeFee;
}
return fee;
}
@@ -377,10 +346,6 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
if (canRollback)
hookExecutionFee +=
calculateHookChainFee(view, tx, keylet::hook(tshAcc));
if (view.rules().enabled(featureHookGas) &&
tx.isFieldPresent(sfHookGas))
hookExecutionFee += XRPAmount{tx.getFieldU32(sfHookGas)};
}
XRPAmount accumulator = baseFee;
@@ -1229,15 +1194,6 @@ Transactor::executeHookChain(
std::map<uint256, std::map<std::vector<uint8_t>, std::vector<uint8_t>>>
hookParamOverrides{};
// Initialize Gas pool for Gas-type hooks
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& hooks = hookSLE->getFieldArray(sfHooks);
uint8_t hook_no = 0;
@@ -1306,19 +1262,6 @@ Transactor::executeHookChain(
bool hasCallback = hookDef->isFieldPresent(sfHookCallbackFee);
// 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)
{
// Pass remaining Gas pool to this hook
hookGas = gasPool;
}
try
{
results.push_back(hook::apply(
@@ -1337,35 +1280,12 @@ Transactor::executeHookChain(
strong,
(strong ? 0 : 1UL), // 0 = strong, 1 = weak
hook_no - 1,
provisionalMeta,
hookApiVersion,
hookGas));
provisionalMeta));
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::ACCEPT)
{
if (results.back().exitType == hook_api::ExitType::WASM_ERROR)
@@ -1502,17 +1422,6 @@ Transactor::doHookCallback(
{
hook::HookStateMap stateMap;
// Extract HookApiVersion for callback
uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion);
// Callbacks don't consume HookGas independently, but we pass it
// for consistency
uint32_t hookGas = 0;
if (ctx_.tx.isFieldPresent(sfHookGas))
{
hookGas = ctx_.tx.getFieldU32(sfHookGas);
}
hook::HookResult callbackResult = hook::apply(
hookDef->getFieldH256(sfHookSetTxnID),
callbackHookHash,
@@ -1532,9 +1441,7 @@ Transactor::doHookCallback(
? 1UL
: 0UL,
hook_no - 1,
provisionalMeta,
hookApiVersion,
hookGas);
provisionalMeta);
executedHookCount_++;
@@ -1810,14 +1717,6 @@ Transactor::doAgainAsWeak(
return;
}
// Extract HookApiVersion for aaw execution
uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion);
// Extract HookGas for Gas-type hooks
uint32_t hookGas = 0;
if (hookApiVersion == 1 && ctx_.tx.isFieldPresent(sfHookGas))
hookGas = ctx_.tx.getFieldU32(sfHookGas);
try
{
hook::HookResult aawResult = hook::apply(
@@ -1836,9 +1735,7 @@ Transactor::doAgainAsWeak(
false,
2UL, // param 2 = aaw
hook_no - 1,
provisionalMeta,
hookApiVersion,
hookGas);
provisionalMeta);
executedHookCount_++;

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 91;
static constexpr std::size_t numFeatures = 90;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -378,7 +378,6 @@ extern uint256 const fixInvalidTxFlags;
extern uint256 const featureExtendedHookState;
extern uint256 const fixCronStacking;
extern uint256 const fixHookAPI20251128;
extern uint256 const featureHookGas;
} // namespace ripple
#endif

View File

@@ -414,8 +414,6 @@ extern SF_UINT32 const sfXahauActivationLgrSeq;
extern SF_UINT32 const sfDelaySeconds;
extern SF_UINT32 const sfRepeatCount;
extern SF_UINT32 const sfStartTime;
extern SF_UINT32 const sfHookGas;
extern SF_UINT32 const sfHookInstructionCost;
// 64-bit integers (common)
extern SF_UINT64 const sfIndexNext;

View File

@@ -344,7 +344,6 @@ enum TECcodes : TERUnderlyingType {
tecIMMUTABLE = 188,
tecTOO_MANY_REMARKS = 189,
tecHAS_HOOK_STATE = 190,
tecHOOK_INSUFFICIENT_GAS = 191,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -484,7 +484,6 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(HookGas, Supported::yes, VoteBehavior::DefaultYes);
// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.

View File

@@ -73,8 +73,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfFlags, soeOPTIONAL},
{sfHookInstructionCost, soeOPTIONAL}});
{sfFlags, soeOPTIONAL}});
add(sfHookEmission.jsonName.c_str(),
sfHookEmission.getCode(),
@@ -92,7 +91,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeREQUIRED},
{sfFlags, soeREQUIRED},
{sfFee, soeOPTIONAL}});
{sfFee, soeREQUIRED}});
add(sfHook.jsonName.c_str(),
sfHook.getCode(),

View File

@@ -158,9 +158,6 @@ CONSTRUCT_TYPED_SFIELD(sfLockCount, "LockCount", UINT32,
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
// 32-bit integers (hook)
CONSTRUCT_TYPED_SFIELD(sfHookInstructionCost, "HookInstructionCost", UINT32, 91);
CONSTRUCT_TYPED_SFIELD(sfHookGas, "HookGas", UINT32, 92);
CONSTRUCT_TYPED_SFIELD(sfStartTime, "StartTime", UINT32, 93);
CONSTRUCT_TYPED_SFIELD(sfRepeatCount, "RepeatCount", UINT32, 94);
CONSTRUCT_TYPED_SFIELD(sfDelaySeconds, "DelaySeconds", UINT32, 95);

View File

@@ -95,7 +95,6 @@ transResults()
MAKE_ERROR(tecIMMUTABLE, "The remark is marked immutable on the object, and therefore cannot be updated."),
MAKE_ERROR(tecTOO_MANY_REMARKS, "The number of remarks on the object would exceed the limit of 32."),
MAKE_ERROR(tecHAS_HOOK_STATE, "Delete all hook state before reducing scale"),
MAKE_ERROR(tecHOOK_INSUFFICIENT_GAS, "Insufficient hook gas to complete the transaction."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),

View File

@@ -44,7 +44,6 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfOperationLimit, soeOPTIONAL},
{sfHookGas, soeOPTIONAL},
};
add(jss::AccountSet,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -41,28 +41,17 @@ echo '
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> wasm = {' > $OUTPUT_FILE
# 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" |
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' |
while read -r line
do
COUNTER=$(cat $COUNTER_FILE)
echo "/* ==== WASM: $COUNTER ==== */" >> $OUTPUT_FILE
echo -n "{ R\"[test.${tag_output}](" >> $OUTPUT_FILE
echo -n '{ R"[test.hook](' >> $OUTPUT_FILE
cat <<< "$line" | sed -E 's/.{7}$//g' | tr -d '\n' | tr '\f' '\n' >> $OUTPUT_FILE
echo ")[test.${tag_output}]\"," >> $OUTPUT_FILE
echo ')[test.hook]",' >> $OUTPUT_FILE
echo "{" >> $OUTPUT_FILE
WAT=`grep -Eo '\(module' <<< $line | wc -l`
if [ "$WAT" -eq "0" ]
@@ -80,19 +69,10 @@ process_block() {
echo "$line"
exit 1
fi
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
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
else
wat2wasm - -o /dev/stdout <<< "`tr '\f' '\n' <<< $(sed -E 's/.{7}$//g' <<< $line)`" |
xxd -p -u -c 10 |
@@ -105,15 +85,8 @@ process_block() {
fi
echo '}},' >> $OUTPUT_FILE
echo >> $OUTPUT_FILE
echo $((COUNTER + 1)) > $COUNTER_FILE
COUNTER=`echo $COUNTER + 1 | bc`
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

@@ -1,50 +0,0 @@
//------------------------------------------------------------------------------
/*
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 <ripple/basics/contract.h>
#include <test/jtx/Env.h>
#include <test/jtx/tags.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;
};
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

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