mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
2327 lines
86 KiB
C++
2327 lines
86 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2014 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <ripple/app/tx/impl/SetHook.h>
|
|
|
|
#include <ripple/app/ledger/Ledger.h>
|
|
#include <ripple/basics/Log.h>
|
|
#include <ripple/ledger/ApplyView.h>
|
|
#include <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/Indexes.h>
|
|
#include <ripple/protocol/STArray.h>
|
|
#include <ripple/protocol/STObject.h>
|
|
#include <ripple/protocol/STTx.h>
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <stdio.h>
|
|
#include <vector>
|
|
#include <stack>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <ripple/app/tx/applyHook.h>
|
|
#include <ripple/app/ledger/LedgerMaster.h>
|
|
#include <ripple/app/ledger/OpenLedger.h>
|
|
#include <functional>
|
|
#include <wasmedge/wasmedge.h>
|
|
#include <exception>
|
|
#include <tuple>
|
|
#include <optional>
|
|
#include <variant>
|
|
|
|
#define DEBUG_GUARD_CHECK 1
|
|
#define HS_ACC() ctx.tx.getAccountID(sfAccount) << "-" << ctx.tx.getTransactionID()
|
|
|
|
namespace ripple {
|
|
//RH UPTO: sethook needs to correctly compute and charge fee for creating new hooks, updating existing hooks
|
|
//and it also needs to account for reserve requirements for namespaces, parameters and grants
|
|
|
|
|
|
// RH TODO test overflow on leb128 detection
|
|
// web assembly contains a lot of run length encoding in LEB128 format
|
|
inline uint64_t
|
|
parseLeb128(std::vector<unsigned char>& buf, int start_offset, int* end_offset)
|
|
{
|
|
uint64_t val = 0, shift = 0, i = start_offset;
|
|
while (i < buf.size())
|
|
{
|
|
int b = (int)(buf[i]);
|
|
uint64_t last = val;
|
|
val += (b & 0x7F) << shift;
|
|
if (val < last)
|
|
{
|
|
// overflow
|
|
throw std::overflow_error { "leb128 overflow" };
|
|
}
|
|
++i;
|
|
if (b & 0x80)
|
|
{
|
|
shift += 7;
|
|
continue;
|
|
}
|
|
*end_offset = i;
|
|
return val;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// this macro will return temMALFORMED if i ever exceeds the end of the hook
|
|
#define CHECK_SHORT_HOOK()\
|
|
{\
|
|
if (i >= hook.size())\
|
|
{\
|
|
JLOG(ctx.j.trace())\
|
|
<< "HookSet(" << hook::log::SHORT_HOOK << ")[" << HS_ACC() << "]: "\
|
|
<< "Malformed transaction: Hook truncated or otherwise invalid. "\
|
|
<< "SetHook.cpp:" << __LINE__;\
|
|
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 {worst case instruction count} if valid or {} if invalid
|
|
// may throw overflow_error
|
|
std::optional<uint64_t>
|
|
check_guard(
|
|
SetHookCtx& ctx,
|
|
ripple::Blob& hook, int codesec,
|
|
int start_offset, int end_offset, int guard_func_idx, int last_import_idx)
|
|
{
|
|
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("\ncheck_guard called with "
|
|
"codesec=%d start_offset=%d end_offset=%d guard_func_idx=%d last_import_idx=%d\n",
|
|
codesec, start_offset, end_offset, guard_func_idx, last_import_idx);
|
|
|
|
if (end_offset <= 0) end_offset = hook.size();
|
|
int block_depth = 0;
|
|
int mode = 1; // controls the state machine for searching for guards
|
|
// 0 = looking for guard from a trigger point (loop or function start)
|
|
// 1 = looking for a new trigger point (loop);
|
|
// currently always starts at 1 no-top-of-func check, see above block comment
|
|
|
|
std::stack<uint64_t> stack; // we track the stack in mode 0 to work out if constants end up in the guard function
|
|
std::map<uint32_t, uint64_t> local_map; // map of local variables since the trigger point
|
|
std::map<uint32_t, uint64_t> global_map; // map of global variables since the trigger point
|
|
|
|
// block depth level -> { largest guard, rolling instruction count }
|
|
std::map<int, std::pair<uint32_t, uint64_t>> instruction_count;
|
|
|
|
// largest guard // instr ccount
|
|
instruction_count[0] = {1, 0};
|
|
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("\n\n\nstart of guard analysis for codesec %d\n", codesec);
|
|
|
|
for (int i = start_offset; i < end_offset; )
|
|
{
|
|
|
|
if (DEBUG_GUARD_CHECK)
|
|
{
|
|
printf("->");
|
|
for (int z = i; z < 16 + i && z < end_offset; ++z)
|
|
printf("%02X", hook[z]);
|
|
printf("\n");
|
|
}
|
|
|
|
CHECK_SHORT_HOOK();
|
|
int instr = hook[i++];
|
|
|
|
instruction_count[block_depth].second++;
|
|
|
|
if (instr == 0x10) // call instr
|
|
{
|
|
int callee_idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - call instruction at %d -- call funcid: %d\n", mode, i, callee_idx);
|
|
|
|
// disallow calling of user defined functions inside a hook
|
|
if (callee_idx > last_import_idx)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::CALL_ILLEGAL << ")[" << HS_ACC() << "]: GuardCheck "
|
|
<< "Hook calls a function outside of the whitelisted imports "
|
|
<< "codesec: " << codesec << " hook byte offset: " << i;
|
|
return {};
|
|
}
|
|
|
|
if (callee_idx == guard_func_idx)
|
|
{
|
|
// found!
|
|
if (mode == 0)
|
|
{
|
|
|
|
if (stack.size() < 2)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::GUARD_PARAMETERS << ")[" << HS_ACC() << "]: GuardCheck "
|
|
<< "_g() called but could not detect constant parameters "
|
|
<< "codesec: " << codesec << " hook byte offset: " << i << "\n";
|
|
return {};
|
|
}
|
|
|
|
uint64_t a = stack.top();
|
|
stack.pop();
|
|
uint64_t b = stack.top();
|
|
stack.pop();
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("FOUND: GUARD(%llu, %llu), codesec: %d offset %d\n", a, b, codesec, i);
|
|
|
|
if (b <= 0)
|
|
{
|
|
// 0 has a special meaning, generally it's not a constant value
|
|
// < 0 is a constant but negative, either way this is a reject condition
|
|
JLOG(ctx.j.trace()) << "HookSet(" << hook::log::GUARD_PARAMETERS << ")"
|
|
<< "[" << HS_ACC() << "]: GuardCheck "
|
|
<< "_g() called but could not detect constant parameters "
|
|
<< "codesec: " << codesec << " hook byte offset: " << i << "\n";
|
|
return {};
|
|
}
|
|
|
|
// update the instruction count for this block depth to the largest possible guard
|
|
if (instruction_count[block_depth].first < a)
|
|
{
|
|
instruction_count[block_depth].first = a;
|
|
if (DEBUG_GUARD_CHECK)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookDebug[" << HS_ACC() << "]: GuardCheck "
|
|
<< "Depth " << block_depth << " guard: " << a;
|
|
}
|
|
}
|
|
|
|
// clear stack and maps
|
|
while (stack.size() > 0)
|
|
stack.pop();
|
|
local_map.clear();
|
|
global_map.clear();
|
|
mode = 1;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (instr == 0x11) // call indirect [ we don't allow guard to be called this way ]
|
|
{
|
|
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 {};
|
|
/*
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - call_indirect instruction at %d\n", mode, i);
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
++i; CHECK_SHORT_HOOK(); //absorb 0x00 trailing
|
|
continue;
|
|
*/
|
|
}
|
|
|
|
// unreachable and nop instructions
|
|
if (instr == 0x00 || instr == 0x01)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - unreachable/nop instruction at %d\n", mode, i);
|
|
continue;
|
|
}
|
|
|
|
// branch loop block instructions
|
|
if ((instr >= 0x02 && instr <= 0x0F) || instr == 0x11)
|
|
{
|
|
if (mode == 0)
|
|
{
|
|
JLOG(ctx.j.trace()) << "HookSet(" << hook::log::GUARD_MISSING << ")"
|
|
<< "[" << HS_ACC() << "]: GuardCheck "
|
|
<< "_g() did not occur at start of function or loop statement "
|
|
<< "codesec: " << codesec << " hook byte offset: " << i;
|
|
return {};
|
|
}
|
|
|
|
// execution to here means we are in 'search mode' for loop instructions
|
|
|
|
// block instruction
|
|
if (instr == 0x02)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - block instruction at %d\n", mode, i);
|
|
|
|
++i; CHECK_SHORT_HOOK();
|
|
block_depth++;
|
|
instruction_count[block_depth] = {1, 0};
|
|
continue;
|
|
}
|
|
|
|
// loop instruction
|
|
if (instr == 0x03)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - loop instruction at %d\n", mode, i);
|
|
|
|
++i; CHECK_SHORT_HOOK();
|
|
mode = 0; // we now search for a guard()
|
|
block_depth++;
|
|
instruction_count[block_depth] = {1, 0};
|
|
continue;
|
|
}
|
|
|
|
// if instr
|
|
if (instr == 0x04)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - if instruction at %d\n", mode, i);
|
|
++i; CHECK_SHORT_HOOK();
|
|
block_depth++;
|
|
instruction_count[block_depth] = {1, 0};
|
|
continue;
|
|
}
|
|
|
|
// else instr
|
|
if (instr == 0x05)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - else instruction at %d\n", mode, i);
|
|
continue;
|
|
}
|
|
|
|
// branch instruction
|
|
if (instr == 0x0C || instr == 0x0D)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - br instruction at %d\n", mode, i);
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
|
|
// branch table instr
|
|
if (instr == 0x0E)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - br_table instruction at %d\n", mode, i);
|
|
int vec_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
for (int v = 0; v < vec_count; ++v)
|
|
{
|
|
parseLeb128(hook, i, &i);
|
|
CHECK_SHORT_HOOK();
|
|
}
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
// parametric instructions | no operands
|
|
if (instr == 0x1A || instr == 0x1B)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - parametric instruction at %d\n", mode, i);
|
|
continue;
|
|
}
|
|
|
|
// variable instructions
|
|
if (instr >= 0x20 && instr <= 0x24)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - variable local/global instruction at %d\n", mode, i);
|
|
|
|
int idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
|
|
if (mode == 1)
|
|
continue;
|
|
|
|
// we need to do stack and map manipualtion to track any possible constants before guard call
|
|
if (instr == 0x20 || instr == 0x23) // local.get idx or global.get idx
|
|
{
|
|
auto& map = ( instr == 0x20 ? local_map : global_map );
|
|
if (map.find(idx) == map.end())
|
|
stack.push(0); // we always put a 0 in place of a local or global we don't know the value of
|
|
else
|
|
stack.push(map[idx]);
|
|
continue;
|
|
}
|
|
|
|
if (instr == 0x21 || instr == 0x22 || instr == 0x24) // local.set idx or global.set idx
|
|
{
|
|
auto& map = ( instr == 0x21 || instr == 0x22 ? local_map : global_map );
|
|
|
|
uint64_t to_store = (stack.size() == 0 ? 0 : stack.top());
|
|
map[idx] = to_store;
|
|
if (instr != 0x22)
|
|
stack.pop();
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// RH TODO support guard consts being passed through memory functions (maybe)
|
|
|
|
//memory instructions
|
|
if (instr >= 0x28 && instr <= 0x3E)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - variable memory instruction at %d\n", mode, i);
|
|
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
|
|
// more memory instructions
|
|
if (instr == 0x3F || instr == 0x40)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - memory instruction at %d\n", mode, i);
|
|
|
|
++i; CHECK_SHORT_HOOK();
|
|
if (instr == 0x40) // disallow memory.grow
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::MEMORY_GROW << ")[" << HS_ACC() << "]: GuardCheck "
|
|
<< "Memory.grow instruction not allowed at "
|
|
<< "codesec: " << codesec << " hook byte offset: " << i << "\n";
|
|
return {};
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// int const instrs
|
|
// numeric instructions with immediates
|
|
if (instr == 0x41 || instr == 0x42)
|
|
{
|
|
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - const instruction at %d\n", mode, i);
|
|
|
|
uint64_t immediate = parseLeb128(hook, i, &i);
|
|
CHECK_SHORT_HOOK(); // RH TODO enforce i32 i64 size limit
|
|
|
|
// in mode 0 we should be stacking our constants and tracking their movement in
|
|
// and out of locals and globals
|
|
stack.push(immediate);
|
|
continue;
|
|
}
|
|
|
|
// const instr
|
|
// more numerics with immediates
|
|
if (instr == 0x43 || instr == 0x44)
|
|
{
|
|
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - const float instruction at %d\n", mode, i);
|
|
|
|
i += ( instr == 0x43 ? 4 : 8 );
|
|
CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
|
|
// numerics no immediates
|
|
if (instr >= 0x45 && instr <= 0xC4)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - numeric instruction at %d\n", mode, i);
|
|
continue;
|
|
}
|
|
|
|
// truncation instructions
|
|
if (instr == 0xFC)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - truncation instruction at %d\n", mode, i);
|
|
i++; CHECK_SHORT_HOOK();
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
|
|
if (instr == 0x0B)
|
|
{
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("%d - block end instruction at %d\n", mode, i);
|
|
|
|
// end of expression
|
|
if (block_depth == 0)
|
|
break;
|
|
|
|
block_depth--;
|
|
|
|
if (block_depth < 0)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::BLOCK_ILLEGAL << ")[" << HS_ACC() << "]: GuardCheck "
|
|
<< "Unexpected 0x0B instruction, malformed"
|
|
<< "codesec: " << codesec << " hook byte offset: " << i;
|
|
return {};
|
|
}
|
|
|
|
// perform the instruction count * guard accounting
|
|
instruction_count[block_depth].second +=
|
|
instruction_count[block_depth+1].second * instruction_count[block_depth+1].first;
|
|
instruction_count.erase(block_depth+1);
|
|
}
|
|
}
|
|
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::INSTRUCTION_COUNT << ")[" << HS_ACC() << "]: GuardCheck "
|
|
<< "Total worse-case execution count: " << instruction_count[0].second;
|
|
|
|
// RH TODO: don't hardcode this
|
|
if (instruction_count[0].second > 0xFFFFF)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::INSTRUCTION_EXCESS << ")[" << HS_ACC() << "]: GuardCheck "
|
|
<< "Maximum possible instructions exceed 1048575, please make your hook smaller "
|
|
<< "or check your guards!";
|
|
return {};
|
|
}
|
|
|
|
// if we reach the end of the code looking for another trigger the guards are installed correctly
|
|
if (mode == 1)
|
|
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 {};
|
|
}
|
|
|
|
bool
|
|
validateHookGrants(SetHookCtx& ctx, STArray const& hookGrants)
|
|
{
|
|
|
|
if (hookGrants.size() < 1)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::GRANTS_EMPTY << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHookGrants empty.";
|
|
return false;
|
|
}
|
|
|
|
if (hookGrants.size() > 8)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::GRANTS_EXCESS << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHookGrants contains more than 8 entries.";
|
|
return false;
|
|
}
|
|
|
|
for (auto const& hookGrant : hookGrants)
|
|
{
|
|
auto const& hookGrantObj = dynamic_cast<STObject const*>(&hookGrant);
|
|
if (!hookGrantObj || (hookGrantObj->getFName() != sfHookGrant))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::GRANTS_ILLEGAL << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHookGrants did not contain sfHookGrant object.";
|
|
return false;
|
|
}
|
|
else if (!hookGrantObj->isFieldPresent(sfAuthorize) && !hookGrantObj->isFieldPresent(sfHookHash))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::GRANTS_FIELD << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHookGrant object did not contain either sfAuthorize "
|
|
<< "or sfHookHash.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
validateHookParams(SetHookCtx& ctx, STArray const& hookParams)
|
|
{
|
|
for (auto const& hookParam : hookParams)
|
|
{
|
|
auto const& hookParamObj = dynamic_cast<STObject const*>(&hookParam);
|
|
|
|
if (!hookParamObj || (hookParamObj->getFName() != sfHookParameter))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::PARAMETERS_ILLEGAL << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: "
|
|
<< "SetHook sfHookParameters contains obj other than sfHookParameter.";
|
|
return false;
|
|
}
|
|
|
|
bool nameFound = false;
|
|
for (auto const& paramElement : *hookParamObj)
|
|
{
|
|
auto const& name = paramElement.getFName();
|
|
|
|
if (name != sfHookParameterName && name != sfHookParameterValue)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::PARAMETERS_FIELD << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: "
|
|
<< "SetHook sfHookParameter contains object other than sfHookParameterName/Value.";
|
|
return false;
|
|
}
|
|
|
|
if (name == sfHookParameterName)
|
|
nameFound = true;
|
|
}
|
|
|
|
if (!nameFound)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::PARAMETERS_NAME << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: "
|
|
<< "SetHook sfHookParameter must contain at least sfHookParameterName";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// infer which operation the user is attempting to execute from the present and absent fields
|
|
HookSetOperation inferOperation(STObject const& hookSetObj)
|
|
{
|
|
uint64_t wasmByteCount = hookSetObj.isFieldPresent(sfCreateCode) ?
|
|
hookSetObj.getFieldVL(sfCreateCode).size() : 0;
|
|
bool hasHash = hookSetObj.isFieldPresent(sfHookHash);
|
|
bool hasCode = hookSetObj.isFieldPresent(sfCreateCode);
|
|
|
|
|
|
if (hasHash && hasCode) // Both HookHash and CreateCode: invalid
|
|
return hsoINVALID;
|
|
else if (hasHash) // Hookhash only: install
|
|
return hsoINSTALL;
|
|
else if (hasCode) // CreateCode only: either delete or create
|
|
return wasmByteCount > 0 ? hsoCREATE : hsoDELETE;
|
|
else if (
|
|
!hasHash && !hasCode &&
|
|
!hookSetObj.isFieldPresent(sfHookGrants) &&
|
|
!hookSetObj.isFieldPresent(sfHookNamespace) &&
|
|
!hookSetObj.isFieldPresent(sfHookParameters) &&
|
|
!hookSetObj.isFieldPresent(sfHookOn) &&
|
|
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
|
|
!hookSetObj.isFieldPresent(sfFlags))
|
|
return hsoNOOP;
|
|
|
|
return hookSetObj.isFieldPresent(sfHookNamespace) ? hsoNSDELETE : hsoUPDATE;
|
|
|
|
}
|
|
|
|
|
|
// may throw overflow_error
|
|
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 {};
|
|
|
|
uint64_t maxInstrCountHook = 0;
|
|
uint64_t maxInstrCountCbak = 0;
|
|
|
|
Blob hook = hookSetObj.getFieldVL(sfCreateCode);
|
|
uint64_t byteCount = hook.size();
|
|
|
|
// RH TODO compute actual smallest possible hook and update this value
|
|
if (byteCount < 10)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::WASM_TOO_SMALL << ")[" << HS_ACC() << "]: "
|
|
<< "Malformed transaction: Hook was not valid webassembly binary. Too small.";
|
|
return {};
|
|
}
|
|
|
|
// check header, magic number
|
|
unsigned char header[8] = { 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U };
|
|
for (int i = 0; i < 8; ++i)
|
|
{
|
|
if (hook[i] != header[i])
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::WASM_BAD_MAGIC << ")[" << HS_ACC() << "]: "
|
|
<< "Malformed transaction: Hook was not valid webassembly binary. "
|
|
<< "Missing magic number or version.";
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// these will store the function type indicies of hook and cbak if
|
|
// hook and cbak are found in the export section
|
|
std::optional<int> hook_func_idx;
|
|
std::optional<int> cbak_func_idx;
|
|
|
|
// this maps function ids to type ids, used for looking up the type of cbak and hook
|
|
// as established inside the wasm binary.
|
|
std::map<int, int> func_type_map;
|
|
|
|
|
|
// now we check for guards... first check if _g is imported
|
|
int guard_import_number = -1;
|
|
int last_import_number = -1;
|
|
int import_count = 0;
|
|
for (int i = 8, j = 0; i < hook.size();)
|
|
{
|
|
|
|
if (j == i)
|
|
{
|
|
// if the loop iterates twice with the same value for i then
|
|
// it's an infinite loop edge case
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::WASM_PARSE_LOOP << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: Hook is invalid WASM binary.";
|
|
return {};
|
|
}
|
|
|
|
j = i;
|
|
|
|
// each web assembly section begins with a single byte section type followed by an leb128 length
|
|
int section_type = hook[i++];
|
|
int section_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
//int section_start = i;
|
|
|
|
if (DEBUG_GUARD_CHECK)
|
|
printf("WASM binary analysis -- upto %d: section %d with length %d\n",
|
|
i, section_type, section_length);
|
|
|
|
int next_section = i + section_length;
|
|
|
|
// we are interested in the import section... we need to know if _g is imported and which import# it is
|
|
if (section_type == 2) // import section
|
|
{
|
|
import_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (import_count <= 0)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "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 {};
|
|
}
|
|
|
|
// process each import one by one
|
|
int func_upto = 0; // not all imports are functions so we need an indep counter for these
|
|
for (int j = 0; j < import_count; ++j)
|
|
{
|
|
// first check module name
|
|
int mod_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (mod_length < 1 || mod_length > (hook.size() - i))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::IMPORT_MODULE_BAD << ")[" << HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook attempted to specify nil or invalid import module";
|
|
return {};
|
|
}
|
|
|
|
if (std::string_view( (const char*)(hook.data() + i), (size_t)mod_length ) != "env")
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::IMPORT_MODULE_ENV << ")[" << HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook attempted to specify import module other than 'env'";
|
|
return {};
|
|
}
|
|
|
|
i += mod_length; CHECK_SHORT_HOOK();
|
|
|
|
// next get import name
|
|
int name_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (name_length < 1 || name_length > (hook.size() - i))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::IMPORT_NAME_BAD << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook attempted to specify nil or invalid import name";
|
|
return {};
|
|
}
|
|
|
|
std::string import_name { (const char*)(hook.data() + i), (size_t)name_length };
|
|
|
|
i += name_length; CHECK_SHORT_HOOK();
|
|
|
|
// next get import type
|
|
if (hook[i] > 0x00)
|
|
{
|
|
// not a function import
|
|
// RH TODO check these other imports for weird stuff
|
|
i++; CHECK_SHORT_HOOK();
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
|
|
// execution to here means it's a function import
|
|
i++; CHECK_SHORT_HOOK();
|
|
/*int type_idx = */
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
|
|
// RH TODO: validate that the parameters of the imported functions are correct
|
|
if (import_name == "_g")
|
|
{
|
|
guard_import_number = func_upto;
|
|
} else if (hook_api::import_whitelist.find(import_name) == hook_api::import_whitelist.end())
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::IMPORT_ILLEGAL << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook attempted to import a function that does not "
|
|
<< "appear in the hook_api function set: `" << import_name << "`";
|
|
return {};
|
|
}
|
|
func_upto++;
|
|
}
|
|
|
|
if (guard_import_number == -1)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::GUARD_IMPORT << ")[" << HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook did not import _g (guard) function";
|
|
return {};
|
|
}
|
|
|
|
last_import_number = func_upto - 1;
|
|
|
|
// we have an imported guard function, so now we need to enforce the guard rules
|
|
// which are:
|
|
// 1. all functions must start with a guard call before any branching [ RH TODO ]
|
|
// 2. all loops must start with a guard call before any branching
|
|
// to enforce these rules we must do a second pass of the wasm in case the function
|
|
// section was placed in this wasm binary before the import section
|
|
|
|
} else
|
|
if (section_type == 7) // export section
|
|
{
|
|
int export_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (export_count <= 0)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::EXPORTS_MISSING << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook did not export any functions... "
|
|
<< "required hook(int64_t), callback(int64_t).";
|
|
return {};
|
|
}
|
|
|
|
for (int j = 0; j < export_count; ++j)
|
|
{
|
|
int name_len = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (name_len == 4)
|
|
{
|
|
|
|
if (hook[i] == 'h' && hook[i+1] == 'o' && hook[i+2] == 'o' && hook[i+3] == 'k')
|
|
{
|
|
i += name_len; CHECK_SHORT_HOOK();
|
|
if (hook[i] != 0)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::EXPORT_HOOK_FUNC << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook did not export: A valid int64_t hook(uint32_t)";
|
|
return {};
|
|
}
|
|
|
|
i++; CHECK_SHORT_HOOK();
|
|
hook_func_idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
|
|
if (hook[i] == 'c' && hook[i+1] == 'b' && hook[i+2] == 'a' && hook[i+3] == 'k')
|
|
{
|
|
i += name_len; CHECK_SHORT_HOOK();
|
|
if (hook[i] != 0)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::EXPORT_CBAK_FUNC << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook did not export: A valid int64_t cbak(uint32_t)";
|
|
return {};
|
|
}
|
|
i++; CHECK_SHORT_HOOK();
|
|
cbak_func_idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
i += name_len + 1;
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
}
|
|
|
|
// execution to here means export section was parsed
|
|
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); " : "" );
|
|
return {};
|
|
}
|
|
}
|
|
else if (section_type == 3) // function section
|
|
{
|
|
int function_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (function_count <= 0)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::FUNCS_MISSING
|
|
<< ")[" << HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook did not establish any functions... "
|
|
<< "required hook(int64_t), callback(int64_t).";
|
|
return {};
|
|
}
|
|
|
|
for (int j = 0; j < function_count; ++j)
|
|
{
|
|
int type_idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
printf("Function map: func %d -> type %d\n", j, type_idx);
|
|
func_type_map[j] = type_idx;
|
|
}
|
|
}
|
|
|
|
i = next_section;
|
|
continue;
|
|
}
|
|
|
|
// we must subtract import_count from the hook and cbak function in order to be able to
|
|
// 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;
|
|
|
|
if (cbak_func_idx)
|
|
*cbak_func_idx -= import_count;
|
|
|
|
if (func_type_map.find(*hook_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 {};
|
|
}
|
|
|
|
int hook_type_idx = func_type_map[*hook_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
|
|
for (int i = 8; i < hook.size();)
|
|
{
|
|
|
|
int section_type = hook[i++];
|
|
int section_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
//int section_start = i;
|
|
int next_section = i + section_length;
|
|
|
|
if (section_type == 1) // type section
|
|
{
|
|
printf("section_type==1 type section\n");
|
|
int type_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
for (int j = 0; j < type_count; ++j)
|
|
{
|
|
if (hook[i++] != 0x60)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::FUNC_TYPE_INVALID << ")["
|
|
<< HS_ACC() << "]: Invalid function type. "
|
|
<< "Codesec: " << section_type << " "
|
|
<< "Local: " << j << " "
|
|
<< "Offset: " << i;
|
|
return {};
|
|
}
|
|
CHECK_SHORT_HOOK();
|
|
|
|
int param_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
for (int k = 0; k < param_count; ++k)
|
|
{
|
|
int param_type = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (param_type == 0x7F || param_type == 0x7E ||
|
|
param_type == 0x7D || param_type == 0x7C)
|
|
{
|
|
// pass, this is fine
|
|
}
|
|
else
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::FUNC_PARAM_INVALID << ")["
|
|
<< HS_ACC() << "]: Invalid parameter type in function type. "
|
|
<< "Codesec: " << section_type << " "
|
|
<< "Local: " << j << " "
|
|
<< "Offset: " << i;
|
|
return {};
|
|
}
|
|
|
|
printf("Function type idx: %d, hook_func_idx: %d, cbak_func_idx: %d "
|
|
"param_count: %d param_type: %x\n",
|
|
j, *hook_func_idx, *cbak_func_idx, param_count, param_type);
|
|
|
|
// hook and cbak parameter check here
|
|
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 {};
|
|
}
|
|
}
|
|
|
|
int result_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
|
|
// RH TODO: enable this for production
|
|
// this needs a reliable hook cleaner otherwise it will catch most compilers out
|
|
if (0 && result_count != 1)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::FUNC_RETURN_COUNT << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< "Hook declares a function type that returns fewer or more than one value.";
|
|
return {};
|
|
}
|
|
|
|
// this can only ever be 1 in production, but in testing it may also be 0 or >1
|
|
// so for completeness this loop is here but can be taken out in prod
|
|
for (int k = 0; k < result_count; ++k)
|
|
{
|
|
int result_type = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (result_type == 0x7F || result_type == 0x7E ||
|
|
result_type == 0x7D || result_type == 0x7C)
|
|
{
|
|
// pass, this is fine
|
|
}
|
|
else
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::FUNC_RETURN_INVALID << ")["
|
|
<< HS_ACC() << "]: Invalid return type in function type. "
|
|
<< "Codesec: " << section_type << " "
|
|
<< "Local: " << j << " "
|
|
<< "Offset: " << i;
|
|
return {};
|
|
}
|
|
|
|
printf("Function type idx: %d, hook_func_idx: %d, cbak_func_idx: %d "
|
|
"result_count: %d result_type: %x\n",
|
|
j, *hook_func_idx, *cbak_func_idx, result_count, result_type);
|
|
|
|
// hook and cbak return type check here
|
|
if ((j == hook_type_idx || (cbak_type_idx && j == cbak_type_idx)) &&
|
|
(result_count != 1 || result_type != 0x7E /* i64 */ ))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::RETURN_HOOK_CBAK << ")["
|
|
<< HS_ACC() << "]: Malformed transaction. "
|
|
<< (j == hook_type_idx ? "hook" : "cbak") << " j=" << j << " "
|
|
<< " function definition must have exactly one int64_t return type. "
|
|
<< "resultcount=" << result_count << ", resulttype=" << result_type << ", "
|
|
<< "paramcount=" << param_count;
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
if (section_type == 10) // code section
|
|
{
|
|
// RH TODO: parse anywhere else an expr is allowed in wasm and enforce rules there too
|
|
// these are the functions
|
|
int func_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
|
|
for (int j = 0; j < func_count; ++j)
|
|
{
|
|
// parse locals
|
|
int code_size = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
int code_end = i + code_size;
|
|
int local_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
for (int k = 0; k < local_count; ++k)
|
|
{
|
|
/*int array_size = */
|
|
parseLeb128(hook, i, &i); CHECK_SHORT_HOOK();
|
|
if (!(hook[i] >= 0x7C && hook[i] <= 0x7F))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::TYPE_INVALID << ")["
|
|
<< HS_ACC() << "]: Invalid local type. "
|
|
<< "Codesec: " << j << " "
|
|
<< "Local: " << k << " "
|
|
<< "Offset: " << i;
|
|
return {};
|
|
}
|
|
i++; CHECK_SHORT_HOOK();
|
|
}
|
|
|
|
if (i == code_end)
|
|
continue; // allow empty functions
|
|
|
|
// execution to here means we are up to the actual expr for the codesec/function
|
|
|
|
auto valid =
|
|
check_guard(ctx, hook, j, i, code_end, guard_import_number, last_import_number);
|
|
|
|
if (!valid)
|
|
return {};
|
|
|
|
if (hook_func_idx && *hook_func_idx == j)
|
|
maxInstrCountHook = *valid;
|
|
else if (cbak_func_idx && *cbak_func_idx == j)
|
|
maxInstrCountCbak = *valid;
|
|
else
|
|
{
|
|
printf("code section: %d not hook_func_idx: %d or cbak_func_idx: %d\n",
|
|
j, *hook_func_idx, (cbak_func_idx ? *cbak_func_idx : -1));
|
|
// assert(false);
|
|
}
|
|
|
|
i = code_end;
|
|
|
|
}
|
|
}
|
|
i = next_section;
|
|
}
|
|
|
|
// 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 "
|
|
<< "size = " << hook.size();
|
|
|
|
std::optional<std::string> result =
|
|
hook::HookExecutor::validateWasm(hook.data(), (size_t)hook.size());
|
|
|
|
if (result)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::WASM_TEST_FAILURE << ")[" << HS_ACC() << "]: "
|
|
<< "Tried to set a hook with invalid code. VM error: " << *result;
|
|
return {};
|
|
}
|
|
|
|
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::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;
|
|
|
|
|
|
switch (inferOperation(hookSetObj))
|
|
{
|
|
case hsoNOOP:
|
|
{
|
|
return true;
|
|
}
|
|
|
|
case hsoNSDELETE:
|
|
{
|
|
// namespace delete operation
|
|
if (hookSetObj.isFieldPresent(sfHookGrants) ||
|
|
hookSetObj.isFieldPresent(sfHookParameters) ||
|
|
hookSetObj.isFieldPresent(sfHookOn) ||
|
|
hookSetObj.isFieldPresent(sfHookApiVersion) ||
|
|
!hookSetObj.isFieldPresent(sfFlags) ||
|
|
!hookSetObj.isFieldPresent(sfHookNamespace))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::NSDELETE_FIELD << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook nsdelete operation should contain only "
|
|
<< "sfHookNamespace & sfFlags";
|
|
return false;
|
|
}
|
|
|
|
if (flags != hsfNSDELETE)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::NSDELETE_FLAGS << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook nsdelete operation should only specify hsfNSDELETE";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
case hsoDELETE:
|
|
{
|
|
if (hookSetObj.isFieldPresent(sfHookGrants) ||
|
|
hookSetObj.isFieldPresent(sfHookParameters) ||
|
|
hookSetObj.isFieldPresent(sfHookOn) ||
|
|
hookSetObj.isFieldPresent(sfHookApiVersion) ||
|
|
hookSetObj.isFieldPresent(sfHookNamespace) ||
|
|
!hookSetObj.isFieldPresent(sfFlags))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::DELETE_FIELD << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook delete operation should contain only sfCreateCode & sfFlags";
|
|
return false;
|
|
}
|
|
|
|
if (!(flags & hsfOVERRIDE))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::OVERRIDE_MISSING << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook delete operation was missing the hsfOVERRIDE flag";
|
|
return false;
|
|
}
|
|
|
|
|
|
if (flags & ~(hsfOVERRIDE | hsfNSDELETE))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::FLAGS_INVALID << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook delete operation specified invalid flags";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
case hsoINSTALL:
|
|
{
|
|
// validate hook params structure, if any
|
|
if (hookSetObj.isFieldPresent(sfHookParameters) &&
|
|
!validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters)))
|
|
return false;
|
|
|
|
// validate hook grants structure, if any
|
|
if (hookSetObj.isFieldPresent(sfHookGrants) &&
|
|
!validateHookGrants(ctx, hookSetObj.getFieldArray(sfHookGrants)))
|
|
return false;
|
|
|
|
// api version not allowed in update
|
|
if (hookSetObj.isFieldPresent(sfHookApiVersion))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::API_ILLEGAL << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook install operation sfHookApiVersion must not be included.";
|
|
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;
|
|
}
|
|
|
|
case hsoUPDATE:
|
|
{
|
|
// must not specify override flag
|
|
if ((flags & hsfOVERRIDE) ||
|
|
((flags & hsfNSDELETE) && !hookSetObj.isFieldPresent(sfHookNamespace)))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "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;
|
|
}
|
|
|
|
// validate hook params structure
|
|
if (hookSetObj.isFieldPresent(sfHookParameters) &&
|
|
!validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters)))
|
|
return false;
|
|
|
|
// validate hook grants structure
|
|
if (hookSetObj.isFieldPresent(sfHookGrants) &&
|
|
!validateHookGrants(ctx, hookSetObj.getFieldArray(sfHookGrants)))
|
|
return false;
|
|
|
|
// api version not allowed in update
|
|
if (hookSetObj.isFieldPresent(sfHookApiVersion))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::API_ILLEGAL << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook update operation sfHookApiVersion must not be included.";
|
|
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;
|
|
}
|
|
|
|
case hsoCREATE:
|
|
{
|
|
// validate hook params structure
|
|
if (hookSetObj.isFieldPresent(sfHookParameters) &&
|
|
!validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters)))
|
|
return false;
|
|
|
|
// validate hook grants structure
|
|
if (hookSetObj.isFieldPresent(sfHookGrants) &&
|
|
!validateHookGrants(ctx, hookSetObj.getFieldArray(sfHookGrants)))
|
|
return false;
|
|
|
|
|
|
// ensure hooknamespace is present
|
|
if (!hookSetObj.isFieldPresent(sfHookNamespace))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::NAMESPACE_MISSING << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHookDefinition must contain sfHookNamespace.";
|
|
return false;
|
|
}
|
|
|
|
// validate api version, if provided
|
|
if (!hookSetObj.isFieldPresent(sfHookApiVersion))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::API_MISSING << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHookApiVersion must be included.";
|
|
return false;
|
|
}
|
|
|
|
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
|
|
if (version != 0)
|
|
{
|
|
// we currently only accept api version 0
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::API_INVALID << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHook->sfHookApiVersion invalid. (Try 0).";
|
|
return false;
|
|
}
|
|
|
|
// validate sfHookOn
|
|
if (!hookSetObj.isFieldPresent(sfHookOn))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOKON_MISSING << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook must include sfHookOn when creating a new hook.";
|
|
return false;
|
|
}
|
|
|
|
// finally validate web assembly byte code
|
|
{
|
|
auto result = validateCreateCode(ctx, hookSetObj);
|
|
if (!result)
|
|
return false;
|
|
return *result;
|
|
}
|
|
}
|
|
|
|
case hsoINVALID:
|
|
default:
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
FeeUnit64
|
|
SetHook::calculateBaseFee(ReadView const& view, STTx const& tx)
|
|
{
|
|
FeeUnit64 extraFee{0};
|
|
|
|
auto const& hookSets = tx.getFieldArray(sfHooks);
|
|
|
|
for (auto const& hookSet : hookSets)
|
|
{
|
|
auto const& hookSetObj = dynamic_cast<STObject const*>(&hookSet);
|
|
|
|
if (!hookSetObj->isFieldPresent(sfCreateCode))
|
|
continue;
|
|
|
|
extraFee += FeeUnit64{
|
|
hook::computeCreationFee(
|
|
hookSetObj->getFieldVL(sfCreateCode).size())};
|
|
}
|
|
|
|
return Transactor::calculateBaseFee(view, tx) + extraFee;
|
|
}
|
|
|
|
TER
|
|
SetHook::preclaim(ripple::PreclaimContext const& ctx)
|
|
{
|
|
|
|
auto const& hookSets = ctx.tx.getFieldArray(sfHooks);
|
|
|
|
for (auto const& hookSet : hookSets)
|
|
{
|
|
|
|
auto const& hookSetObj = dynamic_cast<STObject const*>(&hookSet);
|
|
|
|
if (!hookSetObj->isFieldPresent(sfHookHash))
|
|
continue;
|
|
|
|
auto const& hash = hookSetObj->getFieldH256(sfHookHash);
|
|
{
|
|
if (!ctx.view.exists(keylet::hookDefinition(hash)))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOK_DEF_MISSING << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: No hook exists with the specified hash.";
|
|
return terNO_HOOK;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
NotTEC
|
|
SetHook::preflight(PreflightContext const& ctx)
|
|
{
|
|
|
|
if (!ctx.rules.enabled(featureHooks))
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "HookSet(" << hook::log::AMENDMENT_DISABLED << ")["
|
|
<< HS_ACC() << "]: Hooks Amendment not enabled!";
|
|
return temDISABLED;
|
|
}
|
|
|
|
auto const ret = preflight1(ctx);
|
|
if (!isTesSuccess(ret))
|
|
return ret;
|
|
|
|
if (!ctx.tx.isFieldPresent(sfHooks))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOKS_ARRAY_MISSING << ")["
|
|
<< HS_ACC() << "]: Malformed transaction: SetHook lacked sfHooks array.";
|
|
return temMALFORMED;
|
|
}
|
|
|
|
auto const& hookSets = ctx.tx.getFieldArray(sfHooks);
|
|
|
|
if (hookSets.size() < 1)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOKS_ARRAY_EMPTY << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHooks empty.";
|
|
return temMALFORMED;
|
|
}
|
|
|
|
if (hookSets.size() > hook::maxHookChainLength())
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOKS_ARRAY_TOO_BIG << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHooks contains more than " << hook::maxHookChainLength()
|
|
<< " entries.";
|
|
return temMALFORMED;
|
|
}
|
|
|
|
SetHookCtx shCtx
|
|
{
|
|
.j = ctx.j,
|
|
.tx = ctx.tx,
|
|
.app = ctx.app
|
|
};
|
|
|
|
bool allBlank = true;
|
|
|
|
for (auto const& hookSet : hookSets)
|
|
{
|
|
|
|
auto const& hookSetObj = dynamic_cast<STObject const*>(&hookSet);
|
|
|
|
if (!hookSetObj || (hookSetObj->getFName() != sfHook))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOKS_ARRAY_BAD << ")["
|
|
<< HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHooks contains obj other than sfHook.";
|
|
return temMALFORMED;
|
|
}
|
|
|
|
if (hookSetObj->getCount() == 0) // skip blanks
|
|
continue;
|
|
|
|
allBlank = false;
|
|
|
|
for (auto const& hookSetElement : *hookSetObj)
|
|
{
|
|
auto const& name = hookSetElement.getFName();
|
|
|
|
if (name != sfCreateCode &&
|
|
name != sfHookHash &&
|
|
name != sfHookNamespace &&
|
|
name != sfHookParameters &&
|
|
name != sfHookOn &&
|
|
name != sfHookGrants &&
|
|
name != sfHookApiVersion &&
|
|
name != sfFlags)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHook contains invalid field.";
|
|
return temMALFORMED;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
|
|
// may throw if leb128 overflow is detected
|
|
auto valid =
|
|
validateHookSetEntry(shCtx, *hookSetObj);
|
|
|
|
if (std::holds_alternative<bool>(valid) && !std::get<bool>(valid))
|
|
return temMALFORMED;
|
|
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::WASM_VALIDATION
|
|
<< ")[" << HS_ACC() << "]: Exception: " << e.what();
|
|
return temMALFORMED;
|
|
}
|
|
}
|
|
|
|
if (allBlank)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::HOOKS_ARRAY_BLANK << ")["
|
|
<< HS_ACC()
|
|
<< "]: Malformed transaction: SetHook sfHooks must contain at least one non-blank sfHook.";
|
|
return temMALFORMED;
|
|
}
|
|
|
|
|
|
return preflight2(ctx);
|
|
}
|
|
|
|
|
|
|
|
TER
|
|
SetHook::doApply()
|
|
{
|
|
preCompute();
|
|
return setHook();
|
|
}
|
|
|
|
void
|
|
SetHook::preCompute()
|
|
{
|
|
return Transactor::preCompute();
|
|
}
|
|
|
|
TER
|
|
SetHook::destroyNamespace(
|
|
SetHookCtx& ctx,
|
|
ApplyView& view,
|
|
const AccountID& account,
|
|
uint256 ns
|
|
) {
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::NSDELETE << ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "Destroying Hook Namespace for " << account << " namespace " << ns;
|
|
|
|
Keylet dirKeylet = keylet::hookStateDir(account, ns);
|
|
|
|
std::shared_ptr<SLE const> sleDirNode{};
|
|
unsigned int uDirEntry{0};
|
|
uint256 dirEntry{beast::zero};
|
|
|
|
auto sleDir = view.peek(dirKeylet);
|
|
|
|
if (!sleDir || dirIsEmpty(view, dirKeylet))
|
|
return tesSUCCESS;
|
|
|
|
auto sleAccount = view.peek(keylet::account(account));
|
|
if (!sleAccount)
|
|
{
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::NSDELETE_ACCOUNT
|
|
<< ")[" << HS_ACC() << "]: Account does not exist to destroy namespace from";
|
|
return tefBAD_LEDGER;
|
|
}
|
|
|
|
|
|
if (!cdirFirst(
|
|
view,
|
|
dirKeylet.key,
|
|
sleDirNode,
|
|
uDirEntry,
|
|
dirEntry)) {
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::NSDELETE_DIRECTORY << ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "directory missing ";
|
|
return tefINTERNAL;
|
|
}
|
|
|
|
uint32_t stateCount =sleAccount->getFieldU32(sfHookStateCount);
|
|
uint32_t oldStateCount = stateCount;
|
|
|
|
std::vector<uint256> toDelete {sleDir->getFieldV256(sfIndexes).size()};
|
|
do
|
|
{
|
|
// Make sure any directory node types that we find are the kind
|
|
// we can delete.
|
|
Keylet const itemKeylet{ltCHILD, dirEntry};
|
|
auto sleItem = view.peek(itemKeylet);
|
|
if (!sleItem)
|
|
{
|
|
// Directory node has an invalid index. Bail out.
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::NSDELETE_DIR_ENTRY << ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "directory node in ledger " << view.seq() << " "
|
|
<< "has index to object that is missing: "
|
|
<< to_string(dirEntry);
|
|
return tefBAD_LEDGER;
|
|
}
|
|
|
|
auto nodeType = sleItem->getFieldU16(sfLedgerEntryType);
|
|
|
|
if (nodeType != ltHOOK_STATE)
|
|
{
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::NSDELETE_NONSTATE << ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "directory node in ledger " << view.seq() << " "
|
|
<< "has non-ltHOOK_STATE entry " << to_string(dirEntry);
|
|
return tefBAD_LEDGER;
|
|
}
|
|
|
|
|
|
toDelete.push_back(uint256::fromVoid(itemKeylet.key.data()));
|
|
|
|
} while (cdirNext(view, dirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
|
|
|
// delete it!
|
|
for (auto const& itemKey: toDelete)
|
|
{
|
|
|
|
auto const& sleItem = view.peek({ltHOOK_STATE, itemKey});
|
|
|
|
if (!sleItem)
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "HookSet(" << hook::log::NSDELETE_ENTRY
|
|
<< ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "Namespace ltHOOK_STATE entry was not found in ledger: "
|
|
<< itemKey;
|
|
continue;
|
|
}
|
|
|
|
auto const hint = (*sleItem)[sfOwnerNode];
|
|
if (!view.dirRemove(dirKeylet, hint, itemKey, false))
|
|
{
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::NSDELETE_DIR
|
|
<< ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "directory node in ledger " << view.seq() << " "
|
|
<< "could not be deleted.";
|
|
return tefBAD_LEDGER;
|
|
}
|
|
view.erase(sleItem);
|
|
stateCount--;
|
|
}
|
|
|
|
if (stateCount > oldStateCount)
|
|
{
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::NSDELETE_COUNT << ")[" << HS_ACC() << "]: DeleteState "
|
|
<< "stateCount less than zero (overflow)";
|
|
|
|
return tefBAD_LEDGER;
|
|
}
|
|
|
|
sleAccount->setFieldU32(sfHookStateCount, stateCount);
|
|
|
|
STVector256 const& vec = sleAccount->getFieldV256(sfHookNamespaces);
|
|
if (vec.size() - 1 == 0)
|
|
{
|
|
sleAccount->makeFieldAbsent(sfHookNamespaces);
|
|
}
|
|
else
|
|
{
|
|
std::vector<uint256> nv { vec.size() - 1 };
|
|
|
|
for (uint256 u : vec.value())
|
|
if (u != ns)
|
|
nv.push_back(u);
|
|
|
|
sleAccount->setFieldV256(sfHookNamespaces, STVector256 { std::move(nv) } );
|
|
}
|
|
|
|
view.update(sleAccount);
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
|
|
// returns true if the reference counted ledger entry should be marked for deletion
|
|
// i.e. it has a zero reference count after the decrement is completed
|
|
// otherwise returns false (but still decrements reference count)
|
|
bool reduceReferenceCount(std::shared_ptr<STLedgerEntry>& sle)
|
|
{
|
|
if (sle && sle->isFieldPresent(sfReferenceCount))
|
|
{
|
|
// reduce reference count on reference counted object
|
|
uint64_t refCount = sle->getFieldU64(sfReferenceCount);
|
|
if (refCount > 0)
|
|
{
|
|
refCount--;
|
|
sle->setFieldU64(sfReferenceCount, refCount);
|
|
}
|
|
|
|
return refCount <= 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void incrementReferenceCount(std::shared_ptr<STLedgerEntry>& sle)
|
|
{
|
|
if (sle && sle->isFieldPresent(sfReferenceCount))
|
|
sle->setFieldU64(sfReferenceCount, sle->getFieldU64(sfReferenceCount) + 1);
|
|
}
|
|
|
|
TER
|
|
updateHookParameters(
|
|
SetHookCtx& ctx,
|
|
ripple::STObject const& hookSetObj,
|
|
std::shared_ptr<STLedgerEntry>& oldDefSLE,
|
|
ripple::STObject& newHook)
|
|
{
|
|
const int paramKeyMax = hook::maxHookParameterKeySize();
|
|
const int paramValueMax = hook::maxHookParameterValueSize();
|
|
|
|
std::map<ripple::Blob, ripple::Blob> parameters;
|
|
|
|
// first pull the parameters into a map
|
|
auto const& hookParameters = hookSetObj.getFieldArray(sfHookParameters);
|
|
for (auto const& hookParameter : hookParameters)
|
|
{
|
|
auto const& hookParameterObj = dynamic_cast<STObject const*>(&hookParameter);
|
|
parameters[hookParameterObj->getFieldVL(sfHookParameterName)] =
|
|
hookParameterObj->getFieldVL(sfHookParameterValue);
|
|
}
|
|
|
|
// then erase anything that is the same as the definition's default parameters
|
|
if (parameters.size() > 0)
|
|
{
|
|
auto const& defParameters = oldDefSLE->getFieldArray(sfHookParameters);
|
|
for (auto const& hookParameter : defParameters)
|
|
{
|
|
auto const& hookParameterObj = dynamic_cast<STObject const*>(&hookParameter);
|
|
ripple::Blob n = hookParameterObj->getFieldVL(sfHookParameterName);
|
|
ripple::Blob v = hookParameterObj->getFieldVL(sfHookParameterValue);
|
|
|
|
if (parameters.find(n) != parameters.end() && parameters[n] == v)
|
|
parameters.erase(n);
|
|
}
|
|
}
|
|
|
|
int parameterCount = (int)(parameters.size());
|
|
if (parameterCount > 16)
|
|
{
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::HOOK_PARAMS_COUNT << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: Txn would result in too many parameters on hook";
|
|
return tecINTERNAL;
|
|
}
|
|
|
|
STArray newParameters {sfHookParameters, parameterCount};
|
|
for (const auto& [parameterName, parameterValue] : parameters)
|
|
{
|
|
if (parameterName.size() > paramKeyMax || parameterValue.size() > paramValueMax)
|
|
{
|
|
JLOG(ctx.j.fatal())
|
|
<< "HookSet(" << hook::log::HOOK_PARAM_SIZE << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: Txn would result in a too large parameter name/value on hook";
|
|
return tecINTERNAL;
|
|
}
|
|
|
|
STObject param { sfHookParameter };
|
|
param.setFieldVL(sfHookParameterName, parameterName);
|
|
param.setFieldVL(sfHookParameterValue, parameterValue);
|
|
newParameters.push_back(std::move(param));
|
|
}
|
|
|
|
if (newParameters.size() > 0)
|
|
newHook.setFieldArray(sfHookParameters, std::move(newParameters));
|
|
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
|
|
struct KeyletComparator
|
|
{
|
|
bool operator()(const Keylet& lhs, const Keylet& rhs) const
|
|
{
|
|
return lhs.type < rhs.type || (lhs.type == rhs.type && lhs.key < rhs.key);
|
|
}
|
|
};
|
|
|
|
TER
|
|
SetHook::setHook()
|
|
{
|
|
|
|
/**
|
|
* Each account has optionally an ltHOOK object
|
|
* Which contains an array (sfHooks) of sfHook objects
|
|
* The set hook transaction also contains an array (sfHooks) of sfHook objects
|
|
* These two arrays are mapped 1-1 when updating, inserting or deleting hooks
|
|
* When the user submits a new hook that does not yet exist on the ledger an ltHOOK_DEFINITION object is created
|
|
* Further users setting the same hook code will reference this object using sfHookHash.
|
|
*/
|
|
|
|
SetHookCtx ctx
|
|
{
|
|
.j = ctx_.app.journal("View"),
|
|
.tx = ctx_.tx,
|
|
.app = ctx_.app
|
|
};
|
|
|
|
const int blobMax = hook::maxHookWasmSize();
|
|
auto const accountKeylet = keylet::account(account_);
|
|
auto const hookKeylet = keylet::hook(account_);
|
|
|
|
auto accountSLE = view().peek(accountKeylet);
|
|
|
|
ripple::STArray newHooks{sfHooks, 8};
|
|
auto newHookSLE = std::make_shared<SLE>(hookKeylet);
|
|
|
|
int oldHookCount = 0;
|
|
std::optional<std::reference_wrapper<ripple::STArray const>> oldHooks;
|
|
auto const& oldHookSLE = view().peek(hookKeylet);
|
|
|
|
if (oldHookSLE)
|
|
{
|
|
oldHooks = oldHookSLE->getFieldArray(sfHooks);
|
|
oldHookCount = (oldHooks->get()).size();
|
|
}
|
|
|
|
std::set<ripple::Keylet, KeyletComparator> defsToDestroy {};
|
|
std::set<uint256> namespacesToDestroy {};
|
|
|
|
int hookSetNumber = -1;
|
|
auto const& hookSets = ctx.tx.getFieldArray(sfHooks);
|
|
|
|
int hookSetCount = hookSets.size();
|
|
|
|
for (hookSetNumber = 0; hookSetNumber < std::max(oldHookCount, hookSetCount); ++hookSetNumber)
|
|
{
|
|
|
|
ripple::STObject newHook { sfHook };
|
|
std::optional<std::reference_wrapper<ripple::STObject const>> oldHook;
|
|
// an existing hook would only be present if the array slot also exists on the ltHOOK object
|
|
if (hookSetNumber < oldHookCount)
|
|
oldHook = std::cref((oldHooks->get()[hookSetNumber]).downcast<ripple::STObject const>());
|
|
|
|
std::optional<std::reference_wrapper<ripple::STObject const>> hookSetObj;
|
|
if (hookSetNumber < hookSetCount)
|
|
hookSetObj = std::cref((hookSets[hookSetNumber]).downcast<ripple::STObject const>());
|
|
|
|
std::optional<ripple::uint256> oldNamespace;
|
|
std::optional<ripple::uint256> defNamespace;
|
|
std::optional<ripple::Keylet> oldDirKeylet;
|
|
std::optional<ripple::Keylet> oldDefKeylet;
|
|
std::optional<ripple::Keylet> newDefKeylet;
|
|
std::shared_ptr<STLedgerEntry> oldDefSLE;
|
|
std::shared_ptr<STLedgerEntry> newDefSLE;
|
|
std::shared_ptr<STLedgerEntry> oldDirSLE;
|
|
|
|
std::optional<ripple::uint256> newNamespace;
|
|
std::optional<ripple::Keylet> newDirKeylet;
|
|
|
|
std::optional<uint64_t> oldHookOn;
|
|
std::optional<uint64_t> newHookOn;
|
|
std::optional<uint64_t> defHookOn;
|
|
|
|
// 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
|
|
std::optional<ripple::uint256> createHookHash;
|
|
/**
|
|
* This is the primary HookSet loop. We iterate the sfHooks array inside the txn
|
|
* each entry of this array is available as hookSetObj.
|
|
* Depending on whether or not an existing hook is present in the array slot we are currently up to
|
|
* this hook and its various attributes are available in the optionals prefixed with old.
|
|
* Even if an existing hook is being modified by the sethook obj, we create a newHook obj
|
|
* so a degree of copying is required.
|
|
*/
|
|
|
|
uint32_t flags = 0;
|
|
|
|
if (hookSetObj && hookSetObj->get().isFieldPresent(sfFlags))
|
|
flags = hookSetObj->get().getFieldU32(sfFlags);
|
|
|
|
|
|
HookSetOperation op = hsoNOOP;
|
|
|
|
if (hookSetObj)
|
|
op = inferOperation(hookSetObj->get());
|
|
|
|
printf("HookSet operation %d: %s\n", hookSetNumber,
|
|
(op == hsoNSDELETE ? "hsoNSDELETE" :
|
|
(op == hsoDELETE ? "hsoDELETE" :
|
|
(op == hsoCREATE ? "hsoCREATE" :
|
|
(op == hsoINSTALL ? "hsoINSTALL" :
|
|
(op == hsoUPDATE ? "hsoUPDATE" :
|
|
(op == hsoNOOP ? "hsoNOOP" : "hsoINALID")))))));
|
|
|
|
// if an existing hook exists at this position in the chain then extract the relevant fields
|
|
if (oldHook && oldHook->get().isFieldPresent(sfHookHash))
|
|
{
|
|
oldDefKeylet = keylet::hookDefinition(oldHook->get().getFieldH256(sfHookHash));
|
|
oldDefSLE = view().peek(*oldDefKeylet);
|
|
if (oldDefSLE)
|
|
defNamespace = oldDefSLE->getFieldH256(sfHookNamespace);
|
|
|
|
if (oldHook->get().isFieldPresent(sfHookNamespace))
|
|
oldNamespace = oldHook->get().getFieldH256(sfHookNamespace);
|
|
else if (defNamespace)
|
|
oldNamespace = *defNamespace;
|
|
|
|
oldDirKeylet = keylet::hookStateDir(account_, *oldNamespace);
|
|
oldDirSLE = view().peek(*oldDirKeylet);
|
|
if (oldDefSLE)
|
|
defHookOn = oldDefSLE->getFieldU64(sfHookOn);
|
|
|
|
if (oldHook->get().isFieldPresent(sfHookOn))
|
|
oldHookOn = oldHook->get().getFieldU64(sfHookOn);
|
|
else if (defHookOn)
|
|
oldHookOn = *defHookOn;
|
|
}
|
|
|
|
// in preparation for three way merge populate fields if they are present on the HookSetObj
|
|
if (hookSetObj)
|
|
{
|
|
if (hookSetObj->get().isFieldPresent(sfHookHash))
|
|
{
|
|
newDefKeylet = keylet::hookDefinition(hookSetObj->get().getFieldH256(sfHookHash));
|
|
newDefSLE = view().peek(*newDefKeylet);
|
|
}
|
|
|
|
if (hookSetObj->get().isFieldPresent(sfHookOn))
|
|
newHookOn = hookSetObj->get().getFieldU64(sfHookOn);
|
|
|
|
if (hookSetObj->get().isFieldPresent(sfHookNamespace))
|
|
{
|
|
newNamespace = hookSetObj->get().getFieldH256(sfHookNamespace);
|
|
newDirKeylet = keylet::hookStateDir(account_, *newNamespace);
|
|
}
|
|
}
|
|
|
|
// users may destroy a namespace in any operation except NOOP and INVALID
|
|
if (flags & hsfNSDELETE)
|
|
{
|
|
if (op == hsoNOOP || op == hsoINVALID)
|
|
{
|
|
// don't do any namespace deletion
|
|
}
|
|
else if(op == hsoNSDELETE && newDirKeylet)
|
|
{
|
|
printf("Marking a namespace for destruction.... NSDELETE\n");
|
|
namespacesToDestroy.emplace(*newNamespace);
|
|
}
|
|
else if (oldDirKeylet)
|
|
{
|
|
printf("Marking a namespace for destruction.... non-NSDELETE\n");
|
|
namespacesToDestroy.emplace(*oldNamespace);
|
|
}
|
|
else
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "HookSet(" << hook::log::NSDELETE_NOTHING << ")[" << HS_ACC()
|
|
<< "]: SetHook hsoNSDELETE specified but nothing to delete";
|
|
}
|
|
}
|
|
|
|
|
|
// if there is only an existing hook, without a HookSetObj then it is
|
|
// logically impossible for the operation to not be NOOP
|
|
assert(hookSetObj || op == hsoNOOP);
|
|
|
|
switch (op)
|
|
{
|
|
|
|
case hsoNOOP:
|
|
{
|
|
// if a hook already exists here then migrate it to the new array
|
|
// if it doesn't exist just place a blank object here
|
|
newHooks.push_back( oldHook ? oldHook->get() : ripple::STObject{sfHook} );
|
|
continue;
|
|
}
|
|
|
|
// every case below here is guarenteed to have a populated hookSetObj
|
|
// by the assert statement above
|
|
|
|
case hsoNSDELETE:
|
|
{
|
|
// this case is handled directly above already
|
|
continue;
|
|
}
|
|
|
|
case hsoDELETE:
|
|
{
|
|
|
|
if (!(flags & hsfOVERRIDE))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::DELETE_FLAG << ")[" << HS_ACC()
|
|
<< "]: SetHook delete operation requires hsfOVERRIDE flag";
|
|
return tecREQUIRES_FLAG;
|
|
}
|
|
|
|
// place an empty corresponding Hook
|
|
newHooks.push_back(ripple::STObject{sfHook});
|
|
|
|
if (!oldHook)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::DELETE_NOTHING << ")[" << HS_ACC()
|
|
<< "]: SetHook delete operation deletes non-existent hook";
|
|
|
|
continue;
|
|
}
|
|
|
|
// decrement the hook definition and mark it for deletion if appropriate
|
|
if (oldDefSLE)
|
|
{
|
|
if (reduceReferenceCount(oldDefSLE))
|
|
defsToDestroy.emplace(*oldDefKeylet);
|
|
|
|
view().update(oldDefSLE);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
case hsoUPDATE:
|
|
{
|
|
// set the namespace if it differs from the definition namespace
|
|
if (newNamespace && *defNamespace != *newNamespace)
|
|
newHook.setFieldH256(sfHookNamespace, *newNamespace);
|
|
|
|
// set the hookon field if it differs from definition
|
|
if (newHookOn && *defHookOn != *newHookOn)
|
|
newHook.setFieldU64(sfHookOn, *newHookOn);
|
|
|
|
// parameters
|
|
TER result =
|
|
updateHookParameters(ctx, hookSetObj->get(), oldDefSLE, newHook);
|
|
|
|
if (result != tesSUCCESS)
|
|
return result;
|
|
|
|
// if grants are provided set them
|
|
if (hookSetObj->get().isFieldPresent(sfHookGrants))
|
|
newHook.setFieldArray(sfHookGrants, hookSetObj->get().getFieldArray(sfHookGrants));
|
|
|
|
newHooks.push_back(std::move(newHook));
|
|
continue;
|
|
}
|
|
|
|
|
|
case hsoCREATE:
|
|
{
|
|
if (oldHook && oldHook->get().isFieldPresent(sfHookHash) && !(flags & hsfOVERRIDE))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::CREATE_FLAG << ")[" << HS_ACC()
|
|
<< "]: SetHook create operation would override but hsfOVERRIDE flag wasn't specified";
|
|
return tecREQUIRES_FLAG;
|
|
}
|
|
|
|
|
|
ripple::Blob wasmBytes = hookSetObj->get().getFieldVL(sfCreateCode);
|
|
|
|
if (wasmBytes.size() > blobMax)
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "HookSet(" << hook::log::WASM_TOO_BIG << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook operation would create blob larger than max";
|
|
return tecINTERNAL;
|
|
}
|
|
|
|
createHookHash = ripple::sha512Half_s(
|
|
ripple::Slice(wasmBytes.data(), wasmBytes.size())
|
|
);
|
|
|
|
auto keylet = ripple::keylet::hookDefinition(*createHookHash);
|
|
|
|
|
|
if (view().exists(keylet))
|
|
{
|
|
newDefSLE = view().peek(keylet);
|
|
newDefKeylet = keylet;
|
|
|
|
// this falls through to hsoINSTALL
|
|
}
|
|
else
|
|
{
|
|
uint64_t maxInstrCountHook = 0;
|
|
uint64_t maxInstrCountCbak = 0;
|
|
bool valid = false;
|
|
|
|
// create hook definition SLE
|
|
try
|
|
{
|
|
|
|
auto valid =
|
|
validateHookSetEntry(ctx, hookSetObj->get());
|
|
|
|
// if invalid return an error
|
|
if (std::holds_alternative<bool>(valid))
|
|
{
|
|
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)
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "HookSet(" << hook::log::WASM_INVALID << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: SetHook operation would create invalid hook wasm";
|
|
return tecINTERNAL;
|
|
}
|
|
|
|
// decrement the hook definition and mark it for deletion if appropriate
|
|
if (oldDefSLE)
|
|
{
|
|
if (reduceReferenceCount(oldDefSLE))
|
|
defsToDestroy.emplace(*oldDefKeylet);
|
|
|
|
view().update(oldDefSLE);
|
|
}
|
|
|
|
auto newHookDef = std::make_shared<SLE>( keylet );
|
|
newHookDef->setFieldH256(sfHookHash, *createHookHash);
|
|
newHookDef->setFieldU64( sfHookOn, *newHookOn);
|
|
newHookDef->setFieldH256( sfHookNamespace, *newNamespace);
|
|
newHookDef->setFieldArray( sfHookParameters,
|
|
hookSetObj->get().isFieldPresent(sfHookParameters)
|
|
? hookSetObj->get().getFieldArray(sfHookParameters)
|
|
: STArray {} );
|
|
newHookDef->setFieldU16( sfHookApiVersion,
|
|
hookSetObj->get().getFieldU16(sfHookApiVersion));
|
|
newHookDef->setFieldVL( sfCreateCode, wasmBytes);
|
|
newHookDef->setFieldH256( sfHookSetTxnID, ctx.tx.getTransactionID());
|
|
newHookDef->setFieldU64( sfReferenceCount, 1);
|
|
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));
|
|
continue;
|
|
}
|
|
[[fallthrough]];
|
|
}
|
|
|
|
// the create operation above falls through to this install operation if the sfCreateCode that would
|
|
// otherwise be created already exists on the ledger
|
|
case hsoINSTALL:
|
|
{
|
|
if (oldHook && oldHook->get().isFieldPresent(sfHookHash) && !(flags & hsfOVERRIDE))
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::INSTALL_FLAG << ")[" << HS_ACC()
|
|
<< "]: SetHook install operation would override but hsfOVERRIDE flag wasn't specified";
|
|
return tecREQUIRES_FLAG;
|
|
}
|
|
|
|
// check if the target hook exists
|
|
if (!newDefSLE)
|
|
{
|
|
JLOG(ctx.j.trace())
|
|
<< "HookSet(" << hook::log::INSTALL_MISSING << ")[" << HS_ACC()
|
|
<< "]: SetHook install operation specified HookHash which does not exist on ledger";
|
|
return tecNO_ENTRY;
|
|
}
|
|
|
|
// decrement the hook definition and mark it for deletion if appropriate
|
|
if (oldDefSLE)
|
|
{
|
|
if (reduceReferenceCount(oldDefSLE))
|
|
defsToDestroy.emplace(*oldDefKeylet);
|
|
|
|
view().update(oldDefSLE);
|
|
}
|
|
|
|
// set the hookhash on the new hook, and allow for a fall through event from hsoCREATE
|
|
if (!createHookHash)
|
|
createHookHash = hookSetObj->get().getFieldH256(sfHookHash);
|
|
|
|
newHook.setFieldH256(sfHookHash, *createHookHash);
|
|
|
|
// increment reference count of target HookDefintion
|
|
incrementReferenceCount(newDefSLE);
|
|
|
|
// change which definition we're using to the new target
|
|
defNamespace = newDefSLE->getFieldH256(sfHookNamespace);
|
|
defHookOn = newDefSLE->getFieldU64(sfHookOn);
|
|
|
|
// set the namespace if it differs from the definition namespace
|
|
if (newNamespace && *defNamespace != *newNamespace)
|
|
newHook.setFieldH256(sfHookNamespace, *newNamespace);
|
|
|
|
// set the hookon field if it differs from definition
|
|
if (newHookOn && *defHookOn != *newHookOn)
|
|
newHook.setFieldU64(sfHookOn, *newHookOn);
|
|
|
|
// parameters
|
|
TER result =
|
|
updateHookParameters(ctx, hookSetObj->get(), newDefSLE, newHook);
|
|
|
|
if (result != tesSUCCESS)
|
|
return result;
|
|
|
|
// if grants are provided set them
|
|
if (hookSetObj->get().isFieldPresent(sfHookGrants))
|
|
newHook.setFieldArray(sfHookGrants, hookSetObj->get().getFieldArray(sfHookGrants));
|
|
|
|
newHooks.push_back(std::move(newHook));
|
|
|
|
view().update(newDefSLE);
|
|
|
|
continue;
|
|
}
|
|
|
|
case hsoINVALID:
|
|
default:
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "HookSet(" << hook::log::OPERATION_INVALID << ")[" << HS_ACC()
|
|
<< "]: Malformed transaction: sethook could not understand the desired operation.";
|
|
return tecCLAIM;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// clean up any Namespace directories marked for deletion and any zero reference Hook Definitions
|
|
for (auto const& ns : namespacesToDestroy)
|
|
destroyNamespace(ctx, view(), account_, ns);
|
|
|
|
|
|
// defs
|
|
for (auto const& p : defsToDestroy)
|
|
{
|
|
auto const& sle = view().peek(p);
|
|
if (!sle)
|
|
continue;
|
|
uint64_t refCount = sle->getFieldU64(sfReferenceCount);
|
|
if (refCount <= 0)
|
|
{
|
|
view().erase(sle);
|
|
}
|
|
}
|
|
|
|
// check if the new hook object is empty
|
|
bool newHooksEmpty = true;
|
|
for (auto const& h: newHooks)
|
|
{
|
|
if (h.isFieldPresent(sfHookHash))
|
|
{
|
|
newHooksEmpty = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
newHookSLE->setFieldArray(sfHooks, newHooks);
|
|
newHookSLE->setAccountID(sfAccount, account_);
|
|
|
|
// There are three possible final outcomes
|
|
// Either the account's ltHOOK is deleted, updated or created.
|
|
|
|
if (oldHookSLE && newHooksEmpty)
|
|
{
|
|
// DELETE ltHOOK
|
|
auto const hint = (*oldHookSLE)[sfOwnerNode];
|
|
if (!view().dirRemove(
|
|
keylet::ownerDir(account_),
|
|
hint, hookKeylet.key, false))
|
|
{
|
|
JLOG(j_.fatal())
|
|
<< "HookSet(" << hook::log::HOOK_DELETE << ")[" << HS_ACC()
|
|
<< "]: Unable to delete ltHOOK from owner";
|
|
return tefBAD_LEDGER;
|
|
}
|
|
|
|
view().erase(oldHookSLE);
|
|
|
|
// update owner count to reflect removal
|
|
adjustOwnerCount(view(), accountSLE, -1, j_);
|
|
view().update(accountSLE);
|
|
}
|
|
else if (oldHookSLE && !newHooksEmpty)
|
|
{
|
|
// UPDATE ltHOOK
|
|
view().erase(oldHookSLE);
|
|
view().insert(newHookSLE);
|
|
}
|
|
else if (!oldHookSLE && !newHooksEmpty)
|
|
{
|
|
// CREATE ltHOOK
|
|
auto const page = view().dirInsert(
|
|
keylet::ownerDir(account_),
|
|
hookKeylet,
|
|
describeOwnerDir(account_));
|
|
|
|
JLOG(j_.trace())
|
|
<< "HookSet(" << hook::log::HOOK_ADD << ")[" << HS_ACC()
|
|
<< "]: Adding ltHook to account directory "
|
|
<< to_string(hookKeylet.key) << ": "
|
|
<< (page ? "success" : "failure");
|
|
|
|
if (!page)
|
|
return tecDIR_FULL;
|
|
|
|
newHookSLE->setFieldU64(sfOwnerNode, *page);
|
|
|
|
view().insert(newHookSLE);
|
|
|
|
// update owner count to reflect new ltHOOK object
|
|
adjustOwnerCount(view(), accountSLE, 1, j_);
|
|
view().update(accountSLE);
|
|
}
|
|
else
|
|
{
|
|
// for clarity if this is a NO-OP
|
|
}
|
|
}
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
|
|
} // namespace ripple
|