Compare commits

...

17 Commits

Author SHA1 Message Date
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
25 changed files with 5365 additions and 95 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)

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

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

@@ -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

@@ -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.

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)

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

@@ -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")

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

@@ -124,7 +124,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 +164,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 +177,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;
@@ -325,12 +330,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 +376,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 +423,7 @@ public:
WasmEdge_LogOff();
WasmEdgeVM vm;
WasmEdgeVM vm{hookCtx.result.hookApiVersion};
if (!vm.sane())
{
@@ -433,6 +444,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 +473,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
}

View File

@@ -0,0 +1,438 @@
//------------------------------------------------------------------------------
/*
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]);
// 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 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

@@ -639,7 +639,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)
{

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 "
"sfHookWeakGas must be used with hsfCOLLECT "
"flag.";
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;
}
@@ -1950,6 +2260,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 +2305,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 +2326,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 +2337,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 +2485,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,12 @@ Transactor::Transactor(ApplyContext& ctx)
{
}
XRPAmount
calculateHookGas(uint32_t gas)
{
return XRPAmount{gas};
}
// 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 +260,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 +295,56 @@ 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);
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 +407,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);
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 +471,10 @@ 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));
}
XRPAmount accumulator = baseFee;
@@ -1296,6 +1395,15 @@ 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;
@@ -1360,7 +1468,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 +1511,35 @@ 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::ACCEPT)
{
if (results.back().exitType == hook_api::ExitType::WASM_ERROR)
@@ -1465,7 +1619,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 +1677,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 +1714,9 @@ Transactor::doHookCallback(
? 1UL
: 0UL,
hook_no - 1,
provisionalMeta);
provisionalMeta,
hookApiVersion,
hookGas);
executedHookCount_++;
@@ -1830,6 +2005,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 +2031,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_++;