Skip to content

Commit 08a4c8d

Browse files
committed
Gas Type Hook
1 parent a8d7b26 commit 08a4c8d

File tree

21 files changed

+1807
-115
lines changed

21 files changed

+1807
-115
lines changed

Builds/CMake/RippledCore.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ target_sources (rippled PRIVATE
488488
src/ripple/app/tx/impl/apply.cpp
489489
src/ripple/app/tx/impl/applySteps.cpp
490490
src/ripple/app/hook/impl/applyHook.cpp
491+
src/ripple/app/hook/impl/GasValidator.cpp
491492
src/ripple/app/tx/impl/details/NFTokenUtils.cpp
492493
#[===============================[
493494
main sources:
@@ -909,6 +910,7 @@ if (tests)
909910
src/test/jtx/impl/fee.cpp
910911
src/test/jtx/impl/flags.cpp
911912
src/test/jtx/impl/genesis.cpp
913+
src/test/jtx/impl/hookgas.cpp
912914
src/test/jtx/impl/import.cpp
913915
src/test/jtx/impl/invoice_id.cpp
914916
src/test/jtx/impl/invoke.cpp

hook/sfcodes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
#define sfEmitGeneration ((2U << 16U) + 46U)
6464
#define sfLockCount ((2U << 16U) + 49U)
6565
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
66+
#define sfHookInstructionCost ((2U << 16U) + 91U)
67+
#define sfHookGas ((2U << 16U) + 92U)
6668
#define sfStartTime ((2U << 16U) + 93U)
6769
#define sfRepeatCount ((2U << 16U) + 94U)
6870
#define sfDelaySeconds ((2U << 16U) + 95U)

src/ripple/app/hook/GasValidator.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//------------------------------------------------------------------------------
2+
/*
3+
This file is part of rippled: https://github.com/ripple/rippled
4+
Copyright (c) 2024 XRPL-Labs
5+
6+
Permission to use, copy, modify, and/or distribute this software for any
7+
purpose with or without fee is hereby granted, provided that the above
8+
copyright notice and this permission notice appear in all copies.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
//==============================================================================
19+
20+
#ifndef RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED
21+
#define RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED
22+
23+
#include <ripple/beast/utility/Journal.h>
24+
#include <ripple/protocol/Rules.h>
25+
#include <functional>
26+
#include <optional>
27+
#include <ostream>
28+
#include <string>
29+
#include <vector>
30+
31+
// Forward declaration
32+
using GuardLog =
33+
std::optional<std::reference_wrapper<std::basic_ostream<char>>>;
34+
35+
namespace hook {
36+
37+
/**
38+
* @brief Validate WASM host functions for Gas-type hooks
39+
*
40+
* Validates that a WASM binary only imports allowed host functions
41+
* and does not import the _g (guard) function, which is only for
42+
* Guard-type hooks.
43+
*
44+
* @param wasm The WASM binary to validate
45+
* @param guardLog Logging function for validation errors
46+
* @param guardLogAccStr Account string for logging
47+
* @return std::nullopt if validation succeeds, error message otherwise
48+
*/
49+
std::optional<std::string>
50+
validateWasmHostFunctionsForGas(
51+
std::vector<uint8_t> const& wasm,
52+
ripple::Rules const& rules,
53+
beast::Journal const& j);
54+
55+
} // namespace hook
56+
57+
#endif // RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED

src/ripple/app/hook/applyHook.h

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#include <ripple/protocol/SField.h>
1111
#include <ripple/protocol/TER.h>
1212
#include <ripple/protocol/digest.h>
13-
#include <any>
1413
#include <memory>
1514
#include <optional>
1615
#include <queue>
@@ -462,7 +461,9 @@ apply(
462461
uint32_t wasmParam,
463462
uint8_t hookChainPosition,
464463
// result of apply() if this is weak exec
465-
std::shared_ptr<STObject const> const& provisionalMeta);
464+
std::shared_ptr<STObject const> const& provisionalMeta,
465+
uint16_t hookApiVersion,
466+
uint32_t hookGas);
466467

467468
struct HookContext;
468469

@@ -501,6 +502,7 @@ struct HookResult
501502
std::string exitReason{""};
502503
int64_t exitCode{-1};
503504
uint64_t instructionCount{0};
505+
uint64_t instructionCost{0};
504506
bool hasCallback = false; // true iff this hook wasm has a cbak function
505507
bool isCallback =
506508
false; // true iff this hook execution is a callback in action
@@ -513,6 +515,8 @@ struct HookResult
513515
false; // hook_again allows strong pre-apply to nominate
514516
// additional weak post-apply execution
515517
std::shared_ptr<STObject const> provisionalMeta;
518+
uint16_t hookApiVersion = 0; // 0 = Guard-type, 1 = Gas-type
519+
std::optional<uint64_t> hookGas; // Gas limit for Gas-type hooks
516520
};
517521

518522
class HookExecutor;
@@ -658,6 +662,7 @@ class HookExecutor
658662
if (!conf)
659663
return;
660664
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
665+
WasmEdge_ConfigureStatisticsSetCostMeasuring(conf, true);
661666
ctx = WasmEdge_VMCreate(conf, NULL);
662667
}
663668

@@ -758,6 +763,23 @@ class HookExecutor
758763
return;
759764
}
760765

766+
// Set Gas limit for Gas-type hooks (HookApiVersion == 1)
767+
if (hookCtx.result.hookApiVersion == 1 &&
768+
hookCtx.result.hookGas.has_value())
769+
{
770+
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
771+
if (statsCtx)
772+
{
773+
// Convert HookGas to cost limit count (1 Gas = 1 cost)
774+
uint32_t gasLimit = *hookCtx.result.hookGas;
775+
WasmEdge_StatisticsSetCostLimit(statsCtx, gasLimit);
776+
777+
JLOG(j.trace())
778+
<< "HookInfo[" << HC_ACC() << "]: Set Gas limit to "
779+
<< gasLimit << " cost limit for Gas-type Hook";
780+
}
781+
}
782+
761783
WasmEdge_Value params[1] = {WasmEdge_ValueGenI32((int64_t)wasmParam)};
762784
WasmEdge_Value returns[1];
763785

@@ -771,17 +793,29 @@ class HookExecutor
771793
returns,
772794
1);
773795

796+
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
797+
hookCtx.result.instructionCount =
798+
WasmEdge_StatisticsGetInstrCount(statsCtx);
799+
hookCtx.result.instructionCost =
800+
WasmEdge_StatisticsGetTotalCost(statsCtx);
801+
774802
if (auto err = getWasmError("WASM VM error", res); err)
775803
{
776-
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: " << *err;
804+
JLOG(j.trace()) << "HookError[" << HC_ACC() << "]: " << *err;
805+
806+
// Check if error is due to Gas limit exceeded for Gas-type hooks
807+
if (hookCtx.result.hookApiVersion == 1 &&
808+
err->find("cost limit exceeded") != std::string::npos)
809+
{
810+
JLOG(j.trace()) << "HookError[" << HC_ACC()
811+
<< "]: Gas limit exceeded. Limit was "
812+
<< *hookCtx.result.hookGas << " instructions";
813+
}
814+
777815
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
778816
return;
779817
}
780818

781-
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
782-
hookCtx.result.instructionCount =
783-
WasmEdge_StatisticsGetInstrCount(statsCtx);
784-
785819
// RH NOTE: stack unwind will clean up WasmEdgeVM
786820
}
787821

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//------------------------------------------------------------------------------
2+
/*
3+
This file is part of rippled: https://github.com/ripple/rippled
4+
Copyright (c) 2024 XRPL-Labs
5+
6+
Permission to use, copy, modify, and/or distribute this software for any
7+
purpose with or without fee is hereby granted, provided that the above
8+
copyright notice and this permission notice appear in all copies.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
*/
18+
//==============================================================================
19+
20+
#include <ripple/app/hook/GasValidator.h>
21+
#include <ripple/app/hook/Guard.h>
22+
#include <ripple/app/hook/Macro.h>
23+
#include <ripple/basics/Log.h>
24+
#include <ripple/protocol/Feature.h>
25+
#include <wasmedge/wasmedge.h>
26+
27+
namespace hook {
28+
29+
std::optional<std::string>
30+
validateWasmHostFunctionsForGas(
31+
std::vector<uint8_t> const& wasm,
32+
Rules const& rules,
33+
beast::Journal const& j)
34+
{
35+
// Create WasmEdge Loader
36+
WasmEdge_LoaderContext* loader = WasmEdge_LoaderCreate(NULL);
37+
if (!loader)
38+
{
39+
return "Failed to create WasmEdge Loader";
40+
}
41+
42+
// Parse WASM binary
43+
WasmEdge_ASTModuleContext* astModule = NULL;
44+
WasmEdge_Result res = WasmEdge_LoaderParseFromBuffer(
45+
loader, &astModule, wasm.data(), wasm.size());
46+
47+
if (!WasmEdge_ResultOK(res))
48+
{
49+
WasmEdge_LoaderDelete(loader);
50+
const char* msg = WasmEdge_ResultGetMessage(res);
51+
return std::string("Failed to parse WASM: ") +
52+
(msg ? msg : "unknown error");
53+
}
54+
55+
// Get import count
56+
uint32_t importCount = WasmEdge_ASTModuleListImportsLength(astModule);
57+
58+
if (importCount == 0)
59+
{
60+
WasmEdge_ASTModuleDelete(astModule);
61+
WasmEdge_LoaderDelete(loader);
62+
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORTS_MISSING
63+
<< "): WASM must import at least hook API functions";
64+
return "WASM must import at least hook API functions";
65+
}
66+
67+
// Get imports (max 256)
68+
const WasmEdge_ImportTypeContext* imports[256];
69+
uint32_t actualImportCount = std::min(importCount, 256u);
70+
actualImportCount =
71+
WasmEdge_ASTModuleListImports(astModule, imports, actualImportCount);
72+
73+
std::optional<std::string> error;
74+
75+
// Check each import
76+
for (uint32_t i = 0; i < actualImportCount; i++)
77+
{
78+
WasmEdge_String moduleName =
79+
WasmEdge_ImportTypeGetModuleName(imports[i]);
80+
WasmEdge_String externalName =
81+
WasmEdge_ImportTypeGetExternalName(imports[i]);
82+
WasmEdge_ExternalType extType =
83+
WasmEdge_ImportTypeGetExternalType(imports[i]);
84+
85+
// Only check function imports
86+
if (extType != WasmEdge_ExternalType_Function)
87+
continue;
88+
89+
// Convert WasmEdge_String to std::string for comparison
90+
std::string modName(moduleName.Buf, moduleName.Length);
91+
std::string extName(externalName.Buf, externalName.Length);
92+
93+
// Check module name is "env"
94+
if (modName != "env")
95+
{
96+
JLOG(j.trace())
97+
<< "HookSet(" << hook::log::IMPORT_MODULE_ENV
98+
<< "): Import module must be 'env', found: " << modName;
99+
error = "Import module must be 'env', found: " + modName;
100+
break;
101+
}
102+
103+
// Check for forbidden _g function (guard function)
104+
if (extName == "_g")
105+
{
106+
JLOG(j.trace())
107+
<< "HookSet(" << hook::log::IMPORT_ILLEGAL
108+
<< "): Gas-type hooks cannot import _g (guard) function";
109+
error = "Gas-type hooks cannot import _g (guard) function";
110+
break;
111+
}
112+
113+
// Check external name length
114+
if (extName.length() < 1 || extName.length() > 64)
115+
{
116+
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORT_NAME_BAD
117+
<< "): Import name length invalid: " << extName;
118+
error = "Import name length invalid: " + extName;
119+
break;
120+
}
121+
122+
// Check against whitelist using find()
123+
bool found = false;
124+
125+
// Check base whitelist (import_whitelist)
126+
if (hook_api::import_whitelist.find(extName) !=
127+
hook_api::import_whitelist.end())
128+
{
129+
found = true;
130+
}
131+
132+
// Check extended whitelist (import_whitelist_1)
133+
if (!found && rules.enabled(featureHooksUpdate1) &&
134+
hook_api::import_whitelist_1.find(extName) !=
135+
hook_api::import_whitelist_1.end())
136+
{
137+
found = true;
138+
}
139+
140+
if (!found)
141+
{
142+
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORT_ILLEGAL
143+
<< "): Import not in whitelist: " << extName;
144+
error = "Import not in whitelist: " + extName;
145+
break;
146+
}
147+
}
148+
149+
// Cleanup
150+
WasmEdge_ASTModuleDelete(astModule);
151+
WasmEdge_LoaderDelete(loader);
152+
153+
return error;
154+
}
155+
156+
} // namespace hook

src/ripple/app/hook/impl/applyHook.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,9 @@ hook::apply(
12321232
bool isStrong,
12331233
uint32_t wasmParam,
12341234
uint8_t hookChainPosition,
1235-
std::shared_ptr<STObject const> const& provisionalMeta)
1235+
std::shared_ptr<STObject const> const& provisionalMeta,
1236+
uint16_t hookApiVersion,
1237+
uint32_t hookGas)
12361238
{
12371239
HookContext hookCtx = {
12381240
.applyCtx = applyCtx,
@@ -1264,7 +1266,9 @@ hook::apply(
12641266
.wasmParam = wasmParam,
12651267
.hookChainPosition = hookChainPosition,
12661268
.foreignStateSetDisabled = false,
1267-
.provisionalMeta = provisionalMeta},
1269+
.provisionalMeta = provisionalMeta,
1270+
.hookApiVersion = hookApiVersion,
1271+
.hookGas = hookGas},
12681272
.emitFailure = isCallback && wasmParam & 1
12691273
? std::optional<ripple::STObject>(
12701274
(*(applyCtx.view().peek(keylet::emittedTxn(
@@ -2049,6 +2053,9 @@ hook::finalizeHookResult(
20492053
ripple::Slice{
20502054
hookResult.exitReason.data(), hookResult.exitReason.size()});
20512055
meta.setFieldU64(sfHookInstructionCount, hookResult.instructionCount);
2056+
if (hookResult.hookApiVersion == 1)
2057+
meta.setFieldU32(sfHookInstructionCost, hookResult.instructionCost);
2058+
20522059
meta.setFieldU16(
20532060
sfHookEmitCount,
20542061
emission_txnid.size()); // this will never wrap, hard limit

0 commit comments

Comments
 (0)