Outgoing/Incoming HookOn (#457)

This commit is contained in:
tequ
2026-02-18 08:32:56 +09:00
committed by GitHub
parent efd5f9f6db
commit 67a6970031
14 changed files with 617 additions and 49 deletions

View File

@@ -136,6 +136,8 @@
#define sfEmittedTxnID ((5U << 16U) + 97U)
#define sfHookCanEmit ((5U << 16U) + 96U)
#define sfCron ((5U << 16U) + 95U)
#define sfHookOnIncoming ((5U << 16U) + 94U)
#define sfHookOnOutgoing ((5U << 16U) + 93U)
#define sfAmount ((6U << 16U) + 1U)
#define sfBalance ((6U << 16U) + 2U)
#define sfLimitAmount ((6U << 16U) + 3U)

View File

@@ -89,6 +89,12 @@ canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
ripple::uint256
getHookCanEmit(ripple::STObject const& hookObj, SLE::pointer const& hookDef);
ripple::uint256
getHookOn(
STObject const& obj,
std::shared_ptr<SLE const> const& def,
SField const& field);
struct HookResult;
HookResult

View File

@@ -921,6 +921,23 @@ hook::getHookCanEmit(
return hookCanEmit;
}
ripple::uint256
hook::getHookOn(
STObject const& obj,
std::shared_ptr<SLE const> const& def,
SField const& field)
{
if (obj.isFieldPresent(field))
return obj.getFieldH256(field);
if (obj.isFieldPresent(sfHookOn))
return obj.getFieldH256(sfHookOn);
if (def->isFieldPresent(field))
return def->getFieldH256(field);
if (def->isFieldPresent(sfHookOn))
return def->getFieldH256(sfHookOn);
return uint256{0};
};
// Update HookState ledger objects for the hook... only called after accept()
// assumes the specified acc has already been checked for authoriation (hook
// grants)

View File

@@ -225,7 +225,9 @@ SetHook::inferOperation(STObject const& hookSetObj)
!hasHash && !hasCode && !hookSetObj.isFieldPresent(sfHookGrants) &&
!hookSetObj.isFieldPresent(sfHookNamespace) &&
!hookSetObj.isFieldPresent(sfHookParameters) &&
!hookSetObj.isFieldPresent(sfHookOn) &&
!(hookSetObj.isFieldPresent(sfHookOn) ||
(hookSetObj.isFieldPresent(sfHookOnOutgoing) &&
hookSetObj.isFieldPresent(sfHookOnIncoming))) &&
!hookSetObj.isFieldPresent(sfHookCanEmit) &&
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
!hookSetObj.isFieldPresent(sfFlags))
@@ -261,6 +263,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
if (hookSetObj.isFieldPresent(sfHookGrants) ||
hookSetObj.isFieldPresent(sfHookParameters) ||
hookSetObj.isFieldPresent(sfHookOn) ||
hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming) ||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
!hookSetObj.isFieldPresent(sfFlags) ||
@@ -291,6 +295,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
if (hookSetObj.isFieldPresent(sfHookGrants) ||
hookSetObj.isFieldPresent(sfHookParameters) ||
hookSetObj.isFieldPresent(sfHookOn) ||
hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming) ||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
hookSetObj.isFieldPresent(sfHookNamespace) ||
@@ -448,12 +454,53 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// 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;
if (!ctx.rules.enabled(featureHookOnV2))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook must include "
"sfHookOn before featureHookOnV2 is enabled.";
return false;
}
if (!hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
!hookSetObj.isFieldPresent(sfHookOnIncoming))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook must include "
"sfHookOnOutgoing and sfHookOnIncoming "
"when creating a new hook without sfHookOn.";
return false;
}
auto const outgoing = hookSetObj.getFieldH256(sfHookOnOutgoing);
auto const incoming = hookSetObj.getFieldH256(sfHookOnIncoming);
if (outgoing == incoming)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook outgoing and "
"incoming hookon must be different.";
return false;
}
}
else
{
if (hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook must no"
"include sfHookOnOutgoing and sfHookOnIncoming "
"when creating a new hook with sfHookOn.";
return false;
}
}
// validate sfHookCanEmit
@@ -742,7 +789,8 @@ SetHook::preflight(PreflightContext const& ctx)
if (name != sfCreateCode && name != sfHookHash &&
name != sfHookNamespace && name != sfHookParameters &&
name != sfHookOn && name != sfHookGrants &&
name != sfHookOn && name != sfHookOnOutgoing &&
name != sfHookOnIncoming && name != sfHookGrants &&
name != sfHookApiVersion && name != sfFlags &&
name != sfHookCanEmit)
{
@@ -1264,10 +1312,14 @@ SetHook::setHook()
std::optional<ripple::uint256> newNamespace;
std::optional<ripple::Keylet> newDirKeylet;
std::optional<uint256> oldHookOn;
std::optional<uint256> newHookOn;
std::optional<uint256> defHookOn;
std::optional<uint256> newHookOnOutgoing;
std::optional<uint256> newHookOnIncoming;
std::optional<uint256> defHookOnOutgoing;
std::optional<uint256> defHookOnIncoming;
std::optional<uint256> oldHookCanEmit;
std::optional<uint256> newHookCanEmit;
std::optional<uint256> defHookCanEmit;
@@ -1325,13 +1377,18 @@ SetHook::setHook()
oldDirKeylet = keylet::hookStateDir(account_, *oldNamespace);
oldDirSLE = view().peek(*oldDirKeylet);
if (oldDefSLE)
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookOn))
defHookOn = oldDefSLE->getFieldH256(sfHookOn);
if (oldHook->get().isFieldPresent(sfHookOn))
oldHookOn = oldHook->get().getFieldH256(sfHookOn);
else if (defHookOn)
oldHookOn = *defHookOn;
if (oldDefSLE)
{
if (oldDefSLE->isFieldPresent(sfHookOnOutgoing))
defHookOnOutgoing =
oldDefSLE->getFieldH256(sfHookOnOutgoing);
if (oldDefSLE->isFieldPresent(sfHookOnIncoming))
defHookOnIncoming =
oldDefSLE->getFieldH256(sfHookOnIncoming);
}
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookCanEmit))
defHookCanEmit = oldDefSLE->getFieldH256(sfHookCanEmit);
@@ -1356,6 +1413,14 @@ SetHook::setHook()
if (hookSetObj->get().isFieldPresent(sfHookOn))
newHookOn = hookSetObj->get().getFieldH256(sfHookOn);
if (hookSetObj->get().isFieldPresent(sfHookOnOutgoing))
newHookOnOutgoing =
hookSetObj->get().getFieldH256(sfHookOnOutgoing);
if (hookSetObj->get().isFieldPresent(sfHookOnIncoming))
newHookOnIncoming =
hookSetObj->get().getFieldH256(sfHookOnIncoming);
if (hookSetObj->get().isFieldPresent(sfHookCanEmit))
newHookCanEmit = hookSetObj->get().getFieldH256(sfHookCanEmit);
@@ -1469,6 +1534,14 @@ SetHook::setHook()
if (oldHook->get().isFieldPresent(sfHookOn))
newHook.setFieldH256(
sfHookOn, oldHook->get().getFieldH256(sfHookOn));
if (oldHook->get().isFieldPresent(sfHookOnOutgoing))
newHook.setFieldH256(
sfHookOnOutgoing,
oldHook->get().getFieldH256(sfHookOnOutgoing));
if (oldHook->get().isFieldPresent(sfHookOnIncoming))
newHook.setFieldH256(
sfHookOnIncoming,
oldHook->get().getFieldH256(sfHookOnIncoming));
if (oldHook->get().isFieldPresent(sfHookCanEmit))
newHook.setFieldH256(
sfHookCanEmit,
@@ -1502,6 +1575,24 @@ SetHook::setHook()
newHook.setFieldH256(sfHookOn, *newHookOn);
}
if (newHookOnOutgoing)
{
if (*defHookOnOutgoing == *newHookOnOutgoing)
{
if (newHook.isFieldPresent(sfHookOnOutgoing))
newHook.makeFieldAbsent(sfHookOnOutgoing);
}
}
if (newHookOnIncoming)
{
if (*defHookOnIncoming == *newHookOnIncoming)
{
if (newHook.isFieldPresent(sfHookOnIncoming))
newHook.makeFieldAbsent(sfHookOnIncoming);
}
}
// set the hookcanemit field if it differs from definition
if (newHookCanEmit)
{
@@ -1663,7 +1754,19 @@ SetHook::setHook()
auto newHookDef = std::make_shared<SLE>(keylet);
newHookDef->setFieldH256(sfHookHash, *createHookHash);
newHookDef->setFieldH256(sfHookOn, *newHookOn);
// only HookOn or (HookOnOutgoing and HookOnIncoming)
if (!view().rules().enabled(featureHookOnV2) ||
(!newHookOnOutgoing && !newHookOnIncoming))
newHookDef->setFieldH256(sfHookOn, *newHookOn);
else
{
newHookDef->setFieldH256(
sfHookOnOutgoing, *newHookOnOutgoing);
newHookDef->setFieldH256(
sfHookOnIncoming, *newHookOnIncoming);
}
if (newHookCanEmit)
newHookDef->setFieldH256(
sfHookCanEmit, *newHookCanEmit);
@@ -1759,7 +1862,7 @@ SetHook::setHook()
// change which definition we're using to the new target
defNamespace = newDefSLE->getFieldH256(sfHookNamespace);
defHookOn = newDefSLE->getFieldH256(sfHookOn);
if (newDefSLE->isFieldPresent(sfHookCanEmit))
defHookCanEmit = newDefSLE->getFieldH256(sfHookCanEmit);
@@ -1767,9 +1870,41 @@ SetHook::setHook()
if (newNamespace && *defNamespace != *newNamespace)
newHook.setFieldH256(sfHookNamespace, *newNamespace);
defHookOn = newDefSLE->getFieldH256(sfHookOn);
defHookOnIncoming = newDefSLE->getFieldH256(sfHookOnIncoming);
defHookOnOutgoing = newDefSLE->getFieldH256(sfHookOnOutgoing);
// set the hookon field if it differs from definition
if (newHookOn && *defHookOn != *newHookOn)
newHook.setFieldH256(sfHookOn, *newHookOn);
if (newHookOn)
{
auto const diffFromDef = defHookOn != *newHookOn;
auto const hasIncOutgDef =
*defHookOnIncoming != *defHookOnOutgoing ||
*newHookOn != *defHookOnIncoming;
if (diffFromDef || hasIncOutgDef)
{
newHook.setFieldH256(sfHookOn, *newHookOn);
}
}
// set the incoming/outgoing hookon field if it differs from
// definition
if (newHookOnIncoming || newHookOnOutgoing)
{
auto const diffFromDef =
*defHookOnIncoming != *newHookOnIncoming ||
*defHookOnOutgoing != *newHookOnOutgoing;
auto const hasHookOnDef =
*newHookOnIncoming != *defHookOn ||
*newHookOnOutgoing != *defHookOn;
if (diffFromDef || hasHookOnDef)
{
newHook.setFieldH256(
sfHookOnIncoming, *newHookOnIncoming);
newHook.setFieldH256(
sfHookOnOutgoing, *newHookOnOutgoing);
}
}
// set the hookcanemit field if it differs from definition
if (newHookCanEmit &&
@@ -1825,8 +1960,8 @@ SetHook::setHook()
// sfHook: 1 reserve PER non-blank entry
// sfParameters: 1 reserve PER entry
// sfGrants are: 1 reserve PER entry
// sfHookHash, sfHookNamespace, sfHookOn, sfHookCanEmit,
// sfHookApiVersion, sfFlags: free
// sfHookHash, sfHookNamespace, sfHookOn, sfHookOnOutgoing,
// sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags: free
// sfHookDefinition is not reserved because it is an unowned object,
// rather the uploader is billed via fee according to the following:
@@ -1988,6 +2123,6 @@ SetHook::setHook()
}
return nsDeleteResult;
}
} // namespace ripple
} // namespace ripple

View File

@@ -212,6 +212,7 @@ Transactor::calculateHookChainFee(
ReadView const& view,
STTx const& tx,
Keylet const& hookKeylet,
bool isOutgoing,
bool collectCallsOnly)
{
std::shared_ptr<SLE const> hookSLE = view.read(hookKeylet);
@@ -229,7 +230,7 @@ Transactor::calculateHookChainFee(
uint256 const& hash = hookObj.getFieldH256(sfHookHash);
std::shared_ptr<SLE const> hookDef =
std::shared_ptr<SLE const> const& hookDef =
view.read(keylet::hookDefinition(hash));
// this is an edge case that happens when a hook is deleted and executed
@@ -240,18 +241,16 @@ Transactor::calculateHookChainFee(
continue;
}
// check if the hook can fire
uint256 hookOn =
(hookObj.isFieldPresent(sfHookOn)
? hookObj.getFieldH256(sfHookOn)
: hookDef->getFieldH256(sfHookOn));
uint32_t flags = 0;
if (hookObj.isFieldPresent(sfFlags))
flags = hookObj.getFieldU32(sfFlags);
else
flags = hookDef->getFieldU32(sfFlags);
// check if the hook can fire
uint256 hookOn = hook::getHookOn(
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
if (hook::canHook(tx.getTxnType(), hookOn) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
@@ -335,7 +334,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
}
else
hookExecutionFee += calculateHookChainFee(
view, tx, keylet::hook(tx.getAccountID(sfAccount)));
view, tx, keylet::hook(tx.getAccountID(sfAccount)), true);
// find any additional stakeholders whose hooks will be executed and
// charged to this transaction
@@ -344,8 +343,8 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
for (auto& [tshAcc, canRollback] : tsh)
if (canRollback)
hookExecutionFee +=
calculateHookChainFee(view, tx, keylet::hook(tshAcc));
hookExecutionFee += calculateHookChainFee(
view, tx, keylet::hook(tshAcc), false);
}
XRPAmount accumulator = baseFee;
@@ -1188,6 +1187,7 @@ Transactor::executeHookChain(
std::vector<hook::HookResult>& results,
ripple::AccountID const& account,
bool strong,
bool isOutgoing,
std::shared_ptr<STObject const> const& provisionalMeta)
{
std::set<uint256> hookSkips;
@@ -1222,10 +1222,8 @@ Transactor::executeHookChain(
}
// check if the hook can fire
uint256 hookOn =
(hookObj.isFieldPresent(sfHookOn)
? hookObj.getFieldH256(sfHookOn)
: hookDef->getFieldH256(sfHookOn));
uint256 hookOn = hook::getHookOn(
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
if (!hook::canHook(ctx_.tx.getTxnType(), hookOn))
continue; // skip if it can't
@@ -1244,8 +1242,8 @@ Transactor::executeHookChain(
if (!strong && !(flags & hsfCOLLECT))
continue;
// fetch the namespace either from the hook object of, if absent, the
// hook def
// fetch the namespace either from the hook object of, if absent,
// the hook def
uint256 const& ns =
(hookObj.isFieldPresent(sfHookNamespace)
? hookObj.getFieldH256(sfHookNamespace)
@@ -1587,8 +1585,8 @@ Transactor::doTSH(
continue;
// compute and deduct fees for the TSH if applicable
XRPAmount tshFeeDrops =
calculateHookChainFee(view, ctx_.tx, klTshHook, !canRollback);
XRPAmount tshFeeDrops = calculateHookChainFee(
view, ctx_.tx, klTshHook, false, !canRollback);
// no hooks to execute, skip tsh
if (tshFeeDrops == 0)
@@ -1646,7 +1644,13 @@ Transactor::doTSH(
// execution to here means we can run the TSH's hook chain
TER tshResult = executeHookChain(
tshHook, stateMap, results, tshAccountID, strong, provisionalMeta);
tshHook,
stateMap,
results,
tshAccountID,
strong,
false,
provisionalMeta);
if (canRollback && (!isTesSuccess(tshResult)))
return tshResult;
@@ -1821,7 +1825,13 @@ Transactor::operator()()
if (hooksOriginator && hooksOriginator->isFieldPresent(sfHooks) &&
!ctx_.isEmittedTxn())
result = executeHookChain(
hooksOriginator, stateMap, hookResults, accountID, true, {});
hooksOriginator,
stateMap,
hookResults,
accountID,
true,
true,
{});
if (isTesSuccess(result))
{

View File

@@ -179,6 +179,7 @@ public:
ReadView const& view,
STTx const& tx,
Keylet const& hookKeylet,
bool isOutgoing,
bool collectCallsOnly = false);
protected:
@@ -211,6 +212,7 @@ protected:
std::vector<hook::HookResult>& results,
ripple::AccountID const& account,
bool strong,
bool isOutgoing,
std::shared_ptr<STObject const> const& provisionalMeta);
void

View File

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

View File

@@ -465,6 +465,8 @@ extern SF_UINT256 const sfEmitParentTxnID;
extern SF_UINT256 const sfEmitNonce;
extern SF_UINT256 const sfEmitHookHash;
extern SF_UINT256 const sfObjectID;
extern SF_UINT256 const sfHookOnIncoming;
extern SF_UINT256 const sfHookOnOutgoing;
// 256-bit (uncommon)
extern SF_UINT256 const sfBookDirectory;

View File

@@ -484,6 +484,7 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported

View File

@@ -87,7 +87,9 @@ InnerObjectFormats::InnerObjectFormats()
{{sfCreateCode, soeREQUIRED},
{sfHookNamespace, soeREQUIRED},
{sfHookParameters, soeREQUIRED},
{sfHookOn, soeREQUIRED},
{sfHookOn, soeOPTIONAL},
{sfHookOnIncoming, soeOPTIONAL},
{sfHookOnOutgoing, soeOPTIONAL},
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeREQUIRED},
{sfFlags, soeREQUIRED},
@@ -101,6 +103,8 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookNamespace, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookOn, soeOPTIONAL},
{sfHookOnIncoming, soeOPTIONAL},
{sfHookOnOutgoing, soeOPTIONAL},
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});

View File

@@ -228,7 +228,9 @@ LedgerFormats::LedgerFormats()
ltHOOK_DEFINITION,
{
{sfHookHash, soeREQUIRED},
{sfHookOn, soeREQUIRED},
{sfHookOn, soeOPTIONAL},
{sfHookOnIncoming, soeOPTIONAL},
{sfHookOnOutgoing, soeOPTIONAL},
{sfHookCanEmit, soeOPTIONAL},
{sfHookNamespace, soeREQUIRED},
{sfHookParameters, soeREQUIRED},

View File

@@ -244,6 +244,8 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256,
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95);
CONSTRUCT_TYPED_SFIELD(sfHookOnIncoming, "HookOnIncoming", UINT256, 94);
CONSTRUCT_TYPED_SFIELD(sfHookOnOutgoing, "HookOnOutgoing", UINT256, 93);
// currency amount (common)
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);

View File

@@ -85,6 +85,8 @@ JSS(HookCanEmit); // field
JSS(HookHash); // field
JSS(HookNamespace); // field
JSS(HookOn); // field
JSS(HookOnIncoming); // field
JSS(HookOnOutgoing); // field
JSS(Hooks); // field
JSS(HookGrants); // field
JSS(HookParameters); // field

View File

@@ -238,7 +238,12 @@ public:
using namespace jtx;
Env env{*this, features};
Env env{
*this,
envconfig(),
features,
nullptr,
beast::severities::kDisabled};
auto const alice = Account{"alice"};
auto const gw = Account{"gateway"};
@@ -710,14 +715,20 @@ public:
env.close();
}
// grants, parameters, hookon, hookcanemit, hookapiversion,
// hooknamespace keys must be absent
// grants, parameters, hookon, hookonincoming, hookonoutgoing,
// hookcanemit, hookapiversion, hooknamespace keys must be absent
for (auto const& [key, value] : JSSMap{
{jss::HookGrants, Json::arrayValue},
{jss::HookParameters, Json::arrayValue},
{jss::HookOn,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
{jss::HookOnIncoming,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
{jss::HookOnOutgoing,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
{jss::HookCanEmit,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
@@ -733,7 +744,8 @@ public:
jv[jss::Hooks][0U][jss::Hook] = iv;
env(jv,
M("Hook DELETE operation cannot include: grants, params, "
"hookon, hookcanemit, apiversion, namespace"),
"hookon, HookOnIncoming, HookOnOutgoing, hookcanemit, "
"apiversion, namespace"),
HSFEE,
ter(temMALFORMED));
env.close();
@@ -894,6 +906,12 @@ public:
{jss::HookOn,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
{jss::HookOnIncoming,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
{jss::HookOnOutgoing,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
{jss::HookCanEmit,
"000000000000000000000000000000000000000000000000000000000000"
"0000"},
@@ -910,7 +928,8 @@ public:
jv[jss::Hooks][0U][jss::Hook] = iv;
env(jv,
M("Hook NSDELETE operation cannot include: grants, params, "
"hookon, hookcanemit, apiversion"),
"hookon, hookonincoming, hookonoutgoing, hookcanemit, "
"apiversion"),
HSFEE,
ter(temMALFORMED));
env.close();
@@ -1294,6 +1313,367 @@ public:
: preHookCount + 66);
}
void
testFeeRPC(jtx::Env& env, Json::Value tx, std::string expected)
{
auto const jtx = env.jt(tx);
auto const feeDrops = env.current()->fees().base;
// build tx_blob
Json::Value params;
params[jss::tx_blob] = strHex(jtx.stx->getSerializer().slice());
// fee request
auto const jrr = env.rpc("json", "fee", to_string(params));
// std::cout << "RESULT: " << jrr << "\n";
// verify base fee & open ledger fee
auto const drops = jrr[jss::result][jss::drops];
auto const baseFee = drops[jss::base_fee_no_hooks];
BEAST_EXPECT(baseFee == to_string(feeDrops));
auto const openLedgerFee = drops[jss::open_ledger_fee];
BEAST_EXPECT(openLedgerFee == expected);
// verify hooks fee
auto const hooksFee = jrr[jss::result][jss::fee_hooks_feeunits];
BEAST_EXPECT(hooksFee == expected);
}
void
testHookOnV2(FeatureBitset features)
{
testcase("Test hook on v2");
using namespace jtx;
Env env{*this, features};
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2);
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
env.fund(XRP(10000), alice);
env.fund(XRP(10000), bob);
env.close();
auto const deleteHook = [&env](Account const& account) {
Json::Value jv;
jv[jss::Account] = account.human();
jv[jss::TransactionType] = jss::SetHook;
jv[jss::Flags] = 0;
jv[jss::Hooks] = Json::Value{Json::arrayValue};
Json::Value iv;
iv[jss::CreateCode] = "";
iv[jss::Flags] = hsfOVERRIDE;
jv[jss::Hooks][0U][jss::Hook] = iv;
env(jv, M("hook DELETE"), HSFEE);
env.close();
};
// Disabled
{
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"00000000000000000000000000000000000000000000000000000000000000"
"01";
jv[jss::HookOnOutgoing] =
"00000000000000000000000000000000000000000000000000000000000000"
"02";
// create
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Create: Disabled"),
HSFEE,
!hookOnV2 ? ter(temMALFORMED) : ter(tesSUCCESS));
deleteHook(alice);
// install
env(ripple::test::jtx::hook(bob, {{hso(accept_wasm)}}, 0),
M("Install: Disabled prepare"),
HSFEE);
env.close();
jv[jss::Flags] = hsfOVERRIDE;
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Install: Disabled"),
HSFEE,
!hookOnV2 ? ter(temMALFORMED) : ter(tesSUCCESS));
env.close();
deleteHook(alice);
deleteHook(bob);
// update
env(ripple::test::jtx::hook(alice, {{hso(accept_wasm)}}, 0),
M("Update: Disabled prepare"),
HSFEE);
env.close();
jv[jss::Flags] = hsfOVERRIDE;
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Update: Disabled"),
HSFEE,
!hookOnV2 ? ter(temMALFORMED) : ter(tesSUCCESS));
env.close();
deleteHook(alice);
deleteHook(bob);
}
if (!hookOnV2)
return;
for (int i = 0; i < 3; i++)
{
if (i == 0)
{
// Create
}
if (i == 1)
{
// Install
env(ripple::test::jtx::hook(bob, {{hso(accept_wasm)}}, 0),
M("Install: prepare"),
HSFEE);
env.close();
}
if (i == 2)
{
// Update
env(ripple::test::jtx::hook(alice, {{hso(accept_wasm)}}, 0),
M("Update: prepare"),
HSFEE);
env.close();
}
auto jv = hso(accept_wasm);
jv[jss::Flags] = hsfOVERRIDE;
jv.removeMember(jss::HookOn);
for (auto const& key : {jss::HookOnIncoming, jss::HookOnOutgoing})
{
jv[key] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"00";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Only Incomig/Outgoing HookOn"),
HSFEE,
ter(temMALFORMED));
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"00";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("One Incomig/Outgoing HookOn and HookOn"),
HSFEE,
ter(temMALFORMED));
jv.removeMember(key);
jv.removeMember(jss::HookOn);
}
// Incoming == Outgoing
jv[jss::HookOnIncoming] =
"0000000000000000000000000000000000000000000000000000000000"
"000123";
jv[jss::HookOnOutgoing] =
"0000000000000000000000000000000000000000000000000000000000"
"000123";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Incoming == Outgoing"),
ter(temMALFORMED));
jv.removeMember(jss::HookOnIncoming);
jv.removeMember(jss::HookOnOutgoing);
// HookOn and both Fields
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
jv[jss::HookOnIncoming] =
"0000000000000000000000000000000000000000000000000000000000"
"000001";
jv[jss::HookOnOutgoing] =
"0000000000000000000000000000000000000000000000000000000000"
"000002";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("HookOn and both Fields"),
ter(temMALFORMED));
deleteHook(alice);
deleteHook(bob);
}
// Execution
for (int i = 1; i < 3; i++)
{
if (i == 0)
{
// HookOn from HookDefinition object
}
if (i == 1)
{
// HookOn from Hook Object (definition: incoming/outgoing)
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"00";
jv[jss::HookOnOutgoing] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"01";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
env.close();
}
if (i == 2)
{
// HookOn from Hook Object (definition: HookOn)
auto jv = hso(accept_wasm);
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
env.close();
}
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffffbfff"
"ff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbfff"
"fe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
env.close();
auto hookExecuted = [this, &env]() -> bool {
auto meta = env.meta();
BEAST_EXPECT(meta);
return meta->isFieldPresent(sfHookExecutions);
};
// Check Incoming high
env(invoke::invoke(bob),
invoke::dest(alice),
M("Incoming high"),
fee(XRP(1)));
env.close();
BEAST_EXPECT(hookExecuted());
// Check Incoming low
env(pay(bob, alice, XRP(1)), M("Incoming low"), fee(XRP(1)));
env.close();
BEAST_EXPECT(!hookExecuted());
// Check Outgoing high
env(pay(alice, bob, XRP(1)), M("Outgoing high"), fee(XRP(1)));
env.close();
BEAST_EXPECT(hookExecuted());
// Check Outgoing low
env(invoke::invoke(alice), M("Outgoing high"), fee(XRP(1)));
env.close();
BEAST_EXPECT(!hookExecuted());
deleteHook(alice);
}
{
// sfHookOn from Hook Object (definition: incoming/outgoing)
{
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfff"
"ff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bfff"
"fe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
env.close();
}
auto jv = hso(accept_wasm);
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"00";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
env.close();
auto hookExecuted = [this, &env]() -> bool {
auto meta = env.meta();
BEAST_EXPECT(meta);
return meta->isFieldPresent(sfHookExecutions);
};
// Check Incoming high
env(invoke::invoke(bob),
invoke::dest(alice),
M("Incoming high"),
fee(XRP(1)));
env.close();
BEAST_EXPECT(hookExecuted());
// Check Incoming low
env(pay(bob, alice, XRP(1)), M("Incoming low"), fee(XRP(1)));
env.close();
BEAST_EXPECT(hookExecuted());
// Check Outgoing high
env(pay(alice, bob, XRP(1)), M("Outgoing high"), fee(XRP(1)));
env.close();
BEAST_EXPECT(hookExecuted());
// Check Outgoing low
env(invoke::invoke(alice), M("Outgoing high"), fee(XRP(1)));
env.close();
BEAST_EXPECT(hookExecuted());
deleteHook(alice);
}
// Fee RPC
{
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfff"
"ff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bfff"
"fe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
{
// incoming high
auto tx = invoke::invoke(bob);
tx[jss::Destination] = alice.human();
std::string const feeResult = "19";
testFeeRPC(env, tx, feeResult);
}
{
// incoming low
auto tx = pay(bob, alice, XRP(1));
std::string const feeResult = "10";
testFeeRPC(env, tx, feeResult);
}
{
// outgoing high
auto tx = pay(alice, bob, XRP(1));
std::string const feeResult = "19";
testFeeRPC(env, tx, feeResult);
}
{
// outgoing low
auto tx = invoke::invoke(alice);
std::string const feeResult = "10";
testFeeRPC(env, tx, feeResult);
}
}
}
void
testFillCopy(FeatureBitset features)
{
@@ -13846,6 +14226,8 @@ public:
testNSDeletePartial(features);
testPageCap(features);
testHookOnV2(features);
testFillCopy(features);
testWasm(features);