tsh collect call, compiling not tested

This commit is contained in:
Richard Holland
2022-04-19 12:04:27 +00:00
parent d59c1cac7f
commit 3439888aef
11 changed files with 275 additions and 139 deletions

View File

@@ -38,14 +38,18 @@ namespace hook
struct HookResult;
bool isEmittedTxn(ripple::STTx const& tx);
// this map type acts as both a read and write cache for hook execution
using HookStateMap = std::map<ripple::AccountID, // account that owns the state
std::map<ripple::uint256, // namespace
std::map<ripple::uint256, // key
std::pair<
bool, // is modified from ledger value
ripple::Blob>>>>; // the value
// This map type acts as both a read and write cache for hook execution
// and is preserved across the execution of the set of hook chains
// being executed in the current transaction. It is committed to lgr
// only upon tesSuccess for the otxn.
using HookStateMap =
std::map<
ripple::AccountID, // account that owns the state
std::map<ripple::uint256, // namespace
std::map<ripple::uint256, // key
std::pair<
bool, // is modified from ledger value
ripple::Blob>>>>; // the value
enum TSHFlags : uint8_t
{

View File

@@ -403,6 +403,24 @@ SetAccount::doApply()
}
}
//
// TshCollect
//
if (view().rules().enabled(featureHooks))
{
if (uSetFlag == asfTshCollect)
{
JLOG(j_.trace()) << "Set lsfTshCollect.";
uFlagsOut |= lsfTshCollect;
}
else if (uClearFlag == asfTshCollect)
{
JLOG(j_.trace()) << "Clear lsfTshCollect.";
uFlagsOut &= ~lsfTshCollect;
}
}
//
// EmailHash
//

View File

@@ -42,6 +42,7 @@
#include <exception>
#include <tuple>
#include <optional>
#include <variant>
#define DEBUG_GUARD_CHECK 1
#define HS_ACC() ctx.tx.getAccountID(sfAccount) << "-" << ctx.tx.getTransactionID()
@@ -87,15 +88,15 @@ parseLeb128(std::vector<unsigned char>& buf, int start_offset, int* end_offset)
JLOG(ctx.j.trace())\
<< "HookSet(" << hook::log::SHORT_HOOK << ")[" << HS_ACC() << "]: "\
<< "Malformed transaction: Hook truncated or otherwise invalid\n";\
return {false, 0};\
return {};\
}\
}
// checks the WASM binary for the appropriate required _g guard calls and rejects it if they are not found
// start_offset is where the codesection or expr under analysis begins and end_offset is where it ends
// returns {valid, worst case instruction count}
// returns {worst case instruction count} if valid or {} if invalid
// may throw overflow_error
std::pair<bool, uint64_t>
std::optional<uint64_t>
check_guard(
SetHookCtx& ctx,
ripple::Blob& hook, int codesec,
@@ -149,7 +150,7 @@ check_guard(
<< "HookSet(" << hook::log::CALL_ILLEGAL << ")[" << HS_ACC() << "]: GuardCheck "
<< "Hook calls a function outside of the whitelisted imports "
<< "codesec: " << codesec << " hook byte offset: " << i;
return {false, 0};
return {};
}
if (callee_idx == guard_func_idx)
@@ -164,7 +165,7 @@ check_guard(
<< "HookSet(" << hook::log::GUARD_PARAMETERS << ")[" << HS_ACC() << "]: GuardCheck "
<< "_g() called but could not detect constant parameters "
<< "codesec: " << codesec << " hook byte offset: " << i << "\n";
return {false, 0};
return {};
}
uint64_t a = stack.top();
@@ -182,7 +183,7 @@ check_guard(
<< "[" << HS_ACC() << "]: GuardCheck "
<< "_g() called but could not detect constant parameters "
<< "codesec: " << codesec << " hook byte offset: " << i << "\n";
return {false, 0};
return {};
}
// update the instruction count for this block depth to the largest possible guard
@@ -213,7 +214,7 @@ check_guard(
JLOG(ctx.j.trace()) << "HookSet(" << hook::log::CALL_INDIRECT << ")[" << HS_ACC() << "]: GuardCheck "
<< "Call indirect detected and is disallowed in hooks "
<< "codesec: " << codesec << " hook byte offset: " << i;
return {false, 0};
return {};
/*
if (DEBUG_GUARD_CHECK)
printf("%d - call_indirect instruction at %d\n", mode, i);
@@ -240,7 +241,7 @@ check_guard(
<< "[" << HS_ACC() << "]: GuardCheck "
<< "_g() did not occur at start of function or loop statement "
<< "codesec: " << codesec << " hook byte offset: " << i;
return {false, 0};
return {};
}
// execution to here means we are in 'search mode' for loop instructions
@@ -384,7 +385,7 @@ check_guard(
<< "HookSet(" << hook::log::MEMORY_GROW << ")[" << HS_ACC() << "]: GuardCheck "
<< "Memory.grow instruction not allowed at "
<< "codesec: " << codesec << " hook byte offset: " << i << "\n";
return {false, 0};
return {};
}
continue;
}
@@ -454,7 +455,7 @@ check_guard(
<< "HookSet(" << hook::log::BLOCK_ILLEGAL << ")[" << HS_ACC() << "]: GuardCheck "
<< "Unexpected 0x0B instruction, malformed"
<< "codesec: " << codesec << " hook byte offset: " << i;
return {false, 0};
return {};
}
// perform the instruction count * guard accounting
@@ -475,19 +476,18 @@ check_guard(
<< "HookSet(" << hook::log::INSTRUCTION_EXCESS << ")[" << HS_ACC() << "]: GuardCheck "
<< "Maximum possible instructions exceed 1048575, please make your hook smaller "
<< "or check your guards!";
return {false, 0};
return {};
}
// if we reach the end of the code looking for another trigger the guards are installed correctly
if (mode == 1)
return {true, instruction_count[0].second};
return instruction_count[0].second;
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::GUARD_MISSING << ")[" << HS_ACC() << "]: GuardCheck "
<< "Guard did not occur before end of loop / function. "
<< "Codesec: " << codesec;
return {false, 0};
return {};
}
bool
@@ -611,14 +611,20 @@ HookSetOperation inferOperation(STObject const& hookSetObj)
// may throw overflow_error
std::pair<bool, uint64_t>
std::optional< // unpopulated means invalid
std::pair<
uint64_t, // max instruction count for hook()
uint64_t // max instruction count for cbak()
>>
validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
{
if (!hookSetObj.isFieldPresent(sfCreateCode))
return { false, 0 };
return {};
uint64_t maxInstrCountHook = 0;
uint64_t maxInstrCountCbak = 0;
uint64_t maxInstrCount = 0;
Blob hook = hookSetObj.getFieldVL(sfCreateCode);
uint64_t byteCount = hook.size();
@@ -628,7 +634,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_TOO_SMALL << ")[" << HS_ACC() << "]: "
<< "Malformed transaction: Hook was not valid webassembly binary. Too small.";
return {false, 0};
return {};
}
// check header, magic number
@@ -641,7 +647,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::WASM_BAD_MAGIC << ")[" << HS_ACC() << "]: "
<< "Malformed transaction: Hook was not valid webassembly binary. "
<< "Missing magic number or version.";
return {false, 0};
return {};
}
}
@@ -669,7 +675,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_PARSE_LOOP << ")[" << HS_ACC()
<< "]: Malformed transaction: Hook is invalid WASM binary.";
return {false, 0};
return {};
}
j = i;
@@ -695,7 +701,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::IMPORTS_MISSING << ")[" << HS_ACC() << "]: Malformed transaction. "
<< "Hook did not import any functions... "
<< "required at least guard(uint32_t, uint32_t) and accept or rollback";
return {false, 0};
return {};
}
// process each import one by one
@@ -709,7 +715,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::IMPORT_MODULE_BAD << ")[" << HS_ACC() << "]: Malformed transaction. "
<< "Hook attempted to specify nil or invalid import module";
return {false, 0};
return {};
}
if (std::string_view( (const char*)(hook.data() + i), (size_t)mod_length ) != "env")
@@ -717,7 +723,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::IMPORT_MODULE_ENV << ")[" << HS_ACC() << "]: Malformed transaction. "
<< "Hook attempted to specify import module other than 'env'";
return {false, 0};
return {};
}
i += mod_length; CHECK_SHORT_HOOK();
@@ -730,7 +736,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::IMPORT_NAME_BAD << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook attempted to specify nil or invalid import name";
return {false, 0};
return {};
}
std::string import_name { (const char*)(hook.data() + i), (size_t)name_length };
@@ -763,7 +769,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook attempted to import a function that does not "
<< "appear in the hook_api function set: `" << import_name << "`";
return {false, 0};
return {};
}
func_upto++;
}
@@ -773,7 +779,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::GUARD_IMPORT << ")[" << HS_ACC() << "]: Malformed transaction. "
<< "Hook did not import _g (guard) function";
return {false, 0};
return {};
}
last_import_number = func_upto - 1;
@@ -796,7 +802,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook did not export any functions... "
<< "required hook(int64_t), callback(int64_t).";
return {false, 0};
return {};
}
for (int j = 0; j < export_count; ++j)
@@ -814,7 +820,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::EXPORT_HOOK_FUNC << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook did not export: A valid int64_t hook(uint32_t)";
return {false, 0};
return {};
}
i++; CHECK_SHORT_HOOK();
@@ -831,7 +837,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::EXPORT_CBAK_FUNC << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook did not export: A valid int64_t cbak(uint32_t)";
return {false, 0};
return {};
}
i++; CHECK_SHORT_HOOK();
cbak_func_idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
@@ -844,15 +850,14 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
}
// execution to here means export section was parsed
if (!(hook_func_idx && cbak_func_idx))
if (!hook_func_idx)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::EXPORT_MISSING << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook did not export: " <<
( !hook_func_idx ? "int64_t hook(uint32_t); " : "" ) <<
( !cbak_func_idx ? "int64_t cbak(uint32_t);" : "" );
return {false, 0};
<< "Hook did not export: "
<< ( !hook_func_idx ? "int64_t hook(uint32_t); " : "" );
return {};
}
}
else if (section_type == 3) // function section
@@ -865,7 +870,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< ")[" << HS_ACC() << "]: Malformed transaction. "
<< "Hook did not establish any functions... "
<< "required hook(int64_t), callback(int64_t).";
return {false, 0};
return {};
}
for (int j = 0; j < function_count; ++j)
@@ -884,20 +889,26 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
// look them up in the functions section. this is a rule of the webassembly spec
// note that at this point in execution we are guarenteed these are populated
*hook_func_idx -= import_count;
*cbak_func_idx -= import_count;
if (cbak_func_idx)
*cbak_func_idx -= import_count;
if (func_type_map.find(*hook_func_idx) == func_type_map.end() ||
func_type_map.find(*cbak_func_idx) == func_type_map.end())
(cbak_func_idx && func_type_map.find(*cbak_func_idx) == func_type_map.end()))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::FUNC_TYPELESS << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "hook or cbak functions did not have a corresponding type in WASM binary.";
return {false, 0};
return {};
}
int hook_type_idx = func_type_map[*hook_func_idx];
int cbak_type_idx = func_type_map[*cbak_func_idx];
// cbak function is optional so if it exists it has a type otherwise it is skipped in checks
std::optional<int> cbak_type_idx;
if (cbak_func_idx)
cbak_type_idx = func_type_map[*cbak_func_idx];
// second pass... where we check all the guard function calls follow the guard rules
// minimal other validation in this pass because first pass caught most of it
@@ -923,7 +934,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "Codesec: " << section_type << " "
<< "Local: " << j << " "
<< "Offset: " << i;
return {false, 0};
return {};
}
CHECK_SHORT_HOOK();
@@ -944,7 +955,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "Codesec: " << section_type << " "
<< "Local: " << j << " "
<< "Offset: " << i;
return {false, 0};
return {};
}
printf("Function type idx: %d, hook_func_idx: %d, cbak_func_idx: %d "
@@ -952,14 +963,14 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
j, *hook_func_idx, *cbak_func_idx, param_count, param_type);
// hook and cbak parameter check here
if ((j == hook_type_idx || j == cbak_type_idx) &&
if ((j == hook_type_idx || (cbak_type_idx && j == cbak_type_idx)) &&
(param_count != 1 || param_type != 0x7F /* i32 */ ))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::PARAM_HOOK_CBAK << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "hook and cbak function definition must have exactly one uint32_t parameter.";
return {false, 0};
return {};
}
}
@@ -973,7 +984,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::FUNC_RETURN_COUNT << ")["
<< HS_ACC() << "]: Malformed transaction. "
<< "Hook declares a function type that returns fewer or more than one value.";
return {false, 0};
return {};
}
// this can only ever be 1 in production, but in testing it may also be 0 or >1
@@ -994,7 +1005,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "Codesec: " << section_type << " "
<< "Local: " << j << " "
<< "Offset: " << i;
return {false, 0};
return {};
}
printf("Function type idx: %d, hook_func_idx: %d, cbak_func_idx: %d "
@@ -1002,7 +1013,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
j, *hook_func_idx, *cbak_func_idx, result_count, result_type);
// hook and cbak return type check here
if ((j == hook_type_idx || j == cbak_type_idx) &&
if ((j == hook_type_idx || (cbak_type_idx && j == cbak_type_idx)) &&
(result_count != 1 || result_type != 0x7E /* i64 */ ))
{
JLOG(ctx.j.trace())
@@ -1012,7 +1023,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< " function definition must have exactly one int64_t return type. "
<< "resultcount=" << result_count << ", resulttype=" << result_type << ", "
<< "paramcount=" << param_count;
return {false, 0};
return {};
}
}
}
@@ -1042,7 +1053,7 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
<< "Codesec: " << j << " "
<< "Local: " << k << " "
<< "Offset: " << i;
return {false, 0};
return {};
}
i++; CHECK_SHORT_HOOK();
}
@@ -1052,15 +1063,18 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
// execution to here means we are up to the actual expr for the codesec/function
auto [valid, instruction_count] =
auto valid =
check_guard(ctx, hook, j, i, code_end, guard_import_number, last_import_number);
if (!valid)
return {false, 0};
return {};
// the worst case execution is the fee, this includes the worst case between cbak and hook
if (instruction_count > maxInstrCount)
maxInstrCount = instruction_count;
if (hook_func_idx && *hook_func_idx == j)
maxInstrCountHook = *valid;
else if (cbak_func_idx && *cbak_func_idx == j)
maxInstrCountCbak = *valid;
else
assert(false);
i = code_end;
@@ -1072,7 +1086,9 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
// execution to here means guards are installed correctly
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_SMOKE_TEST << ")[" << HS_ACC() << "]: Trying to wasm instantiate proposed hook "
<< "HookSet("
<< hook::log::WASM_SMOKE_TEST
<< ")[" << HS_ACC() << "]: Trying to wasm instantiate proposed hook "
<< "size = " << hook.size();
std::optional<std::string> result =
@@ -1083,19 +1099,24 @@ validateCreateCode(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_TEST_FAILURE << ")[" << HS_ACC() << "]: "
<< "Tried to set a hook with invalid code. VM error: " << *result;
return {false, 0};
return {};
}
return {true, maxInstrCount};
return std::pair<uint64_t, uint64_t>{maxInstrCountHook, maxInstrCountCbak};
}
// This is a context-free validation, it does not take into account the current state of the ledger
// returns < valid, instruction count >
// may throw overflow_error
std::pair<bool, uint64_t>
std::variant<
bool, // true = valid
std::pair< // if set implicitly valid, and return instruction counts (hsoCREATE only)
uint64_t, // max instruction count for hook
uint64_t // max instruction count for cbak
>
>
validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
{
uint32_t flags = hookSetObj.isFieldPresent(sfFlags) ? hookSetObj.getFieldU32(sfFlags) : 0;
@@ -1103,7 +1124,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
{
case hsoNOOP:
{
return {true, 0};
return true;
}
case hsoNSDELETE:
@@ -1120,7 +1141,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::NSDELETE_FIELD << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook nsdelete operation should contain only "
<< "sfHookNamespace & sfFlags";
return {false, 0};
return false;
}
if (flags != hsfNSDELETE)
@@ -1128,10 +1149,10 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::NSDELETE_FLAGS << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook nsdelete operation should only specify hsfNSDELETE";
return {false, 0};
return false;
}
return {true, 0};
return true;
}
case hsoDELETE:
@@ -1146,7 +1167,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::DELETE_FIELD << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook delete operation should contain only sfCreateCode & sfFlags";
return {false, 0};
return false;
}
if (!(flags & hsfOVERRIDE))
@@ -1154,7 +1175,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::OVERRIDE_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook delete operation was missing the hsfOVERRIDE flag";
return {false, 0};
return false;
}
@@ -1163,10 +1184,10 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::FLAGS_INVALID << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook delete operation specified invalid flags";
return {false, 0};
return false;
}
return {true, 0};
return true;
}
case hsoINSTALL:
@@ -1174,12 +1195,12 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// validate hook params structure, if any
if (hookSetObj.isFieldPresent(sfHookParameters) &&
!validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters)))
return {false, 0};
return false;
// validate hook grants structure, if any
if (hookSetObj.isFieldPresent(sfHookGrants) &&
!validateHookGrants(ctx, hookSetObj.getFieldArray(sfHookGrants)))
return {false, 0};
return false;
// api version not allowed in update
if (hookSetObj.isFieldPresent(sfHookApiVersion))
@@ -1187,14 +1208,14 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::API_ILLEGAL << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook install operation sfHookApiVersion must not be included.";
return {false, 0};
return false;
}
// 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
return {true, 0};
return true;
}
case hsoUPDATE:
@@ -1207,18 +1228,18 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
<< "HookSet(" << hook::log::FLAGS_INVALID << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook update operation only hsfNSDELETE may be specified and "
<< "only if a new HookNamespace is also specified.";
return {false, 0};
return false;
}
// validate hook params structure
if (hookSetObj.isFieldPresent(sfHookParameters) &&
!validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters)))
return {false, 0};
return false;
// validate hook grants structure
if (hookSetObj.isFieldPresent(sfHookGrants) &&
!validateHookGrants(ctx, hookSetObj.getFieldArray(sfHookGrants)))
return {false, 0};
return false;
// api version not allowed in update
if (hookSetObj.isFieldPresent(sfHookApiVersion))
@@ -1226,14 +1247,14 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::API_ILLEGAL << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook update operation sfHookApiVersion must not be included.";
return {false, 0};
return false;
}
// 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
return {true, 0};
return true;
}
case hsoCREATE:
@@ -1241,12 +1262,12 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// validate hook params structure
if (hookSetObj.isFieldPresent(sfHookParameters) &&
!validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters)))
return {false, 0};
return false;
// validate hook grants structure
if (hookSetObj.isFieldPresent(sfHookGrants) &&
!validateHookGrants(ctx, hookSetObj.getFieldArray(sfHookGrants)))
return {false, 0};
return false;
// ensure hooknamespace is present
@@ -1255,7 +1276,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::NAMESPACE_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook sfHookDefinition must contain sfHookNamespace.";
return {false, 0};
return false;
}
// validate api version, if provided
@@ -1264,7 +1285,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::API_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook sfHookApiVersion must be included.";
return {false, 0};
return false;
}
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
@@ -1274,7 +1295,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::API_INVALID << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook sfHook->sfHookApiVersion invalid. (Try 0).";
return {false, 0};
return false;
}
// validate sfHookOn
@@ -1283,11 +1304,16 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook must include sfHookOn when creating a new hook.";
return {false, 0};
return false;
}
// finally validate web assembly byte code
return validateCreateCode(ctx, hookSetObj);
{
auto result = validateCreateCode(ctx, hookSetObj);
if (!result)
return false;
return *result;
}
}
case hsoINVALID:
@@ -1296,7 +1322,7 @@ validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HASH_OR_CODE << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook must provide only one of sfCreateCode or sfHookHash.";
return {false, 0};
return false;
}
}
}
@@ -1447,12 +1473,12 @@ SetHook::preflight(PreflightContext const& ctx)
{
// may throw if leb128 overflow is detected
[[maybe_unused]]
auto [valid, _] =
auto valid =
validateHookSetEntry(shCtx, *hookSetObj);
if (!valid)
return temMALFORMED;
if (std::holds_alternative<bool>(valid) && !std::get<bool>(valid))
return temMALFORMED;
}
catch (std::exception& e)
{
@@ -2036,23 +2062,34 @@ SetHook::setHook()
}
else
{
uint64_t maxInstrCount = 0;
uint64_t maxInstrCountHook = 0;
uint64_t maxInstrCountCbak = 0;
bool valid = false;
// create hook definition SLE
try
{
std::tie(valid, maxInstrCount) =
auto valid =
validateHookSetEntry(ctx, hookSetObj->get());
if (!valid)
// if invalid return an error
if (std::holds_alternative<bool>(valid))
{
JLOG(ctx.j.warn())
<< "HookSet(" << hook::log::WASM_INVALID << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook operation would create invalid hook wasm";
return tecINTERNAL;
if (!std::get<bool>(valid))
{
JLOG(ctx.j.warn())
<< "HookSet(" << hook::log::WASM_INVALID << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook operation would create invalid hook wasm";
return tecINTERNAL;
}
else
assert(false); // should never happen
}
// otherwise assign instruction counts
std::tie(maxInstrCountHook, maxInstrCountCbak) =
std::get<std::pair<uint64_t, uint64_t>>(valid);
}
catch (std::exception& e)
{
@@ -2084,7 +2121,11 @@ SetHook::setHook()
newHookDef->setFieldVL( sfCreateCode, wasmBytes);
newHookDef->setFieldH256( sfHookSetTxnID, ctx.tx.getTransactionID());
newHookDef->setFieldU64( sfReferenceCount, 1);
newHookDef->setFieldAmount(sfFee, XRPAmount {hook::computeExecutionFee(maxInstrCount)} );
newHookDef->setFieldAmount(sfFee,
XRPAmount {hook::computeExecutionFee(maxInstrCountHook)});
if (maxInstrCountCbak > 0)
newHookDef->setFieldAmount(sfHookCallbackFee,
XRPAmount {hook::computeExecutionFee(maxInstrCountCbak)});
view().insert(newHookDef);
newHook.setFieldH256(sfHookHash, *createHookHash);
newHooks.push_back(std::move(newHook));

View File

@@ -228,16 +228,16 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
// if the txn is NOT an emitted txn then we process the sending account's hook chain
if (tx.isFieldPresent(sfEmitDetails))
{
STObject const& emitDetails =
const_cast<ripple::STTx&>(tx).getField(sfEmitDetails).downcast<STObject>();
uint256 const& callbackHookHash = emitDetails.getFieldH256(sfEmitHookHash);
std::shared_ptr<SLE const> hookDef = view.read(keylet::hookDefinition(callbackHookHash));
if (hookDef)
hookExecutionFee += FeeUnit64{(uint32_t)(hookDef->getFieldAmount(sfFee).xrp().drops())};
if (hookDef && hookDef->isFieldPresent(sfHookCallbackFee))
hookExecutionFee +=
FeeUnit64{(uint32_t)(hookDef->getFieldAmount(sfHookCallbackFee).xrp().drops())};
}
else
hookExecutionFee +=
@@ -896,7 +896,7 @@ static __inline__ unsigned long long rdtsc(void)
return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}
bool /* error = true */
bool /* retval of true means an error */
gatherHookParameters(
std::shared_ptr<ripple::STLedgerEntry> const& hookDef,
ripple::STObject const* hookObj,
@@ -933,7 +933,7 @@ gatherHookParameters(
return false;
}
bool /* error = true */
bool /* retval of true means an error */
executeHookChain(
std::shared_ptr<ripple::STLedgerEntry const> const& hookSLE,
std::shared_ptr<hook::HookStateMap>& stateMap,
@@ -1095,12 +1095,12 @@ Transactor::operator()()
// and any associated chains which are executed during this transaction also
std::shared_ptr<hook::HookStateMap> stateMap = std::make_shared<hook::HookStateMap>();
auto const& ledger = ctx_.view();
auto& view = ctx_.view();
auto const& accountID = ctx_.tx.getAccountID(sfAccount);
std::vector<hook::HookResult> sendResults;
std::vector<hook::HookResult> recvResults;
auto const& hooksSending = ledger.read(keylet::hook(accountID));
auto const& hooksSending = view.read(keylet::hook(accountID));
// First check if the Sending account has any hooks that can be fired
if (hooksSending && hooksSending->isFieldPresent(sfHooks) && !ctx_.emitted())
@@ -1111,23 +1111,91 @@ Transactor::operator()()
if (!rollback)
{
std::vector<std::pair<AccountID, bool>> tsh =
hook::getTransactionalStakeHolders(ctx_.tx, ledger);
hook::getTransactionalStakeHolders(ctx_.tx, view);
for (auto& [tshAccountID, canRollback] : tsh)
{
auto const& hooksReceiving = ledger.read(keylet::hook(tshAccountID));
if (hooksReceiving && hooksReceiving->isFieldPresent(sfHooks))
auto klTshHook = keylet::hook(tshAccountID);
auto tshHook = view.read(klTshHook);
if (!(tshHook && tshHook->isFieldPresent(sfHooks)))
continue;
// scoping here allows tshAcc to leave scope before
// hook execution, which is probably safer
{
bool tshRollback =
executeHookChain(hooksReceiving, stateMap,
recvResults, executedHookCount, tshAccountID, ctx_, j_, result);
if (canRollback && tshRollback)
// check if the TSH exists and/or has any hooks
auto tshAcc = view.peek(keylet::account(tshAccountID));
if (!tshAcc)
continue;
// compute and deduct fees for the TSH if applicable
FeeUnit64 tshFee =
calculateHookChainFee(view, ctx_.tx, klTshHook);
// no hooks to execute, skip tsh
if (tshFee == 0)
continue;
XRPAmount tshFeeDrops = view.fees().toDrops(tshFee);
assert(tshFeeDrops >= beast::zero);
STAmount priorBalance = tshAcc->getFieldAmount(sfBalance);
if (canRollback)
{
rollback = true;
break;
// this is not a collect call so we will force the tsh's fee to 0
// the otxn paid the fee for this tsh chain execution already.
tshFeeDrops = 0;
}
else
{
// this is a collect call so first check if the tsh can accept
uint32_t tshFlags = tshAcc->getFieldU32(sfFlags);
if (!canRollback && !(tshFlags & lsfTshCollect))
{
// this TSH doesn't allow collect calls, skip
JLOG(j_.trace())
<< "HookInfo[" << accountID << "]: TSH acc " << tshAccountID << " "
<< "hook chain execution skipped due to lack of lsfTshCollect flag.";
continue;
}
// now check if they can afford this collect call
auto const uOwnerCount = tshAcc->getFieldU32(sfOwnerCount);
auto const reserve = view.fees().accountReserve(uOwnerCount);
if (tshFeeDrops + reserve > priorBalance)
{
JLOG(j_.trace())
<< "HookInfo[" << accountID << "]: TSH acc " << tshAccountID << " "
<< "hook chain execution skipped due to lack of TSH acc funds.";
continue;
}
}
//RH TODO: charge non-rollback tsh executions a fee here
if (tshFeeDrops > beast::zero)
{
STAmount finalBalance = priorBalance -= tshFeeDrops;
assert(finalBalance >= beast::zero);
assert(finalBalance < priorBalance);
tshAcc->setFieldAmount(sfBalance, finalBalance);
view.update(tshAcc);
}
}
// execution to here means we can run the TSH's hook chain
bool tshRollback =
executeHookChain(tshHook, stateMap,
recvResults, executedHookCount, tshAccountID, ctx_, j_, result);
if (canRollback && tshRollback)
{
rollback = true;
break;
}
}
}
@@ -1153,19 +1221,16 @@ Transactor::operator()()
auto const& emitDetails =
const_cast<ripple::STTx&>(ctx_.tx).getField(sfEmitDetails).downcast<STObject>();
// callbacks are optional so if there isn't a callback then skip
if (!emitDetails.isFieldPresent(sfEmitCallback))
{
JLOG(j_.fatal())
<< "HookError[]: Callback Processing: Failure: sfEmitCallback missing";
break;
}
AccountID const& callbackAccountID = emitDetails.getAccountID(sfEmitCallback);
uint256 const& callbackHookHash = emitDetails.getFieldH256(sfEmitHookHash);
auto const& hooksCallback = view().peek(keylet::hook(callbackAccountID));
auto const& hookDef = view().peek(keylet::hookDefinition(callbackHookHash));
auto const& hooksCallback = view.peek(keylet::hook(callbackAccountID));
auto const& hookDef = view.peek(keylet::hookDefinition(callbackHookHash));
if (!hookDef)
{
JLOG(j_.warn())
@@ -1173,6 +1238,14 @@ Transactor::operator()()
break;
}
if (!hookDef->isFieldPresent(sfHookCallbackFee))
{
JLOG(j_.trace())
<< "HookInfo[" << callbackAccountID << "]: Callback specified by emitted txn "
<< "but hook lacks a cbak function, skipping.";
break;
}
if (!hooksCallback)
{
JLOG(j_.warn())
@@ -1232,7 +1305,6 @@ Transactor::operator()()
callbackHookHash,
ns,
hookDef->getFieldVL(sfCreateCode),
// params
parameters,
{},
stateMap,
@@ -1273,7 +1345,8 @@ Transactor::operator()()
if (!found)
{
JLOG(j_.warn())
<< "HookError[]: Hookhash not found on callback account";
<< "HookError[" << callbackAccountID << "]: Hookhash "
<< callbackHookHash << " not found on callback account";
}
}
}

View File

@@ -77,9 +77,6 @@ namespace hook
case ttTICKET_CREATE:
case ttHOOK_SET:
case ttOFFER_CREATE: // this is handled seperately
//case ttCONTRACT: // not used
//case ttSPINAL_TAP: // not used
//case ttNICKNAME_SET: // not used
{
break;
}

View File

@@ -242,10 +242,9 @@ enum LedgerSpecificFlags {
lsfNoFreeze = 0x00200000, // True, cannot freeze ripple states
lsfGlobalFreeze = 0x00400000, // True, all assets frozen
lsfDefaultRipple =
0x00800000, // True, trust lines allow rippling by default
lsfDepositAuth = 0x01000000, // True, all deposits require authorization
lsfHookEnabled = 0x02000000, // True, all in and out tx will fire hook code
0x00800000, // True, trust lines allow rippling by default
lsfDepositAuth = 0x01000000, // True, all deposits require authorization
lsfTshCollect = 0x02000000, // True, allow TSH collect-calls to acc hooks
// ltOFFER
lsfPassive = 0x00010000,

View File

@@ -479,6 +479,7 @@ extern SF_AMOUNT const sfMinimumOffer;
extern SF_AMOUNT const sfRippleEscrow;
extern SF_AMOUNT const sfDeliveredAmount;
extern SF_AMOUNT const sfBrokerFee;
extern SF_AMOUNT const sfHookCallbackFee;
// variable length (common)
extern SF_VL const sfPublicKey;

View File

@@ -71,6 +71,7 @@ const std::uint32_t asfNoFreeze = 6;
const std::uint32_t asfGlobalFreeze = 7;
const std::uint32_t asfDefaultRipple = 8;
const std::uint32_t asfDepositAuth = 9;
const std::uint32_t asfTshCollect = 11;
// OfferCreate flags:
const std::uint32_t tfPassive = 0x00010000;

View File

@@ -30,7 +30,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfEmitBurden, soeREQUIRED},
{sfEmitParentTxnID, soeREQUIRED},
{sfEmitNonce, soeREQUIRED},
{sfEmitCallback, soeREQUIRED},
{sfEmitCallback, soeOPTIONAL},
{sfEmitHookHash, soeREQUIRED}
});

View File

@@ -203,7 +203,8 @@ LedgerFormats::LedgerFormats()
{sfCreateCode, soeREQUIRED},
{sfHookSetTxnID, soeREQUIRED},
{sfReferenceCount, soeREQUIRED},
{sfFee, soeREQUIRED}
{sfFee, soeREQUIRED},
{sfHookCallbackFee, soeOPTIONAL}
},
commonFields);

View File

@@ -231,6 +231,7 @@ CONSTRUCT_TYPED_SFIELD(sfMinimumOffer, "MinimumOffer", AMOUNT,
CONSTRUCT_TYPED_SFIELD(sfRippleEscrow, "RippleEscrow", AMOUNT, 17);
CONSTRUCT_TYPED_SFIELD(sfDeliveredAmount, "DeliveredAmount", AMOUNT, 18);
CONSTRUCT_TYPED_SFIELD(sfBrokerFee, "BrokerFee", AMOUNT, 19);
CONSTRUCT_TYPED_SFIELD(sfHookCallbackFee, "HookCallbackFee", AMOUNT, 20);
// variable length (common)
CONSTRUCT_TYPED_SFIELD(sfPublicKey, "PublicKey", VL, 1);