Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Holland
51f8a9c9ac init 2026-06-21 13:35:55 +10:00
18 changed files with 104 additions and 1337 deletions

View File

@@ -297,7 +297,6 @@
#define sfRemark ((14U << 16U) + 97U)
#define sfHighReward ((14U << 16U) + 98U)
#define sfLowReward ((14U << 16U) + 99U)
#define sfNamedHook ((14U << 16U) + 100U)
#define sfSigners ((15U << 16U) + 3U)
#define sfSignerEntries ((15U << 16U) + 4U)
#define sfTemplate ((15U << 16U) + 5U)
@@ -326,4 +325,3 @@
#define sfActiveValidators ((15U << 16U) + 95U)
#define sfGenesisMints ((15U << 16U) + 96U)
#define sfRemarks ((15U << 16U) + 97U)
#define sfHookNames ((15U << 16U) + 98U)

View File

@@ -116,18 +116,6 @@ maxNamespaceDelete(void)
return 256;
}
// maximum number of HookName selectors carried in a transaction's sfHookNames
// array (featureNamedHooks). Kept small: at most maxHookChainLength() named
// hooks can exist on any one account, and a cron re-emits its carried selector
// array on every execution, so an oversized array would amplify pseudo-txn
// size. 32 leaves ample headroom for selecting across a sender + several TSH
// accounts in a single transaction.
inline uint32_t
maxNamedHookSelectors(void)
{
return 32;
}
enum TSHFlags : uint8_t {
tshNONE = 0b000,
tshROLLBACK = 0b001,

View File

@@ -34,6 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(SecureUI, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -66,8 +66,6 @@ LEDGER_ENTRY_DUPLICATE(ltCRON, 0x0041, Cron, cron, ({
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfHookName, soeOPTIONAL},
{sfHookNames, soeOPTIONAL},
}))
/** A ledger object which describes a check.
@@ -264,6 +262,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfAMMID, soeOPTIONAL},
{sfSecureUI, soeOPTIONAL},
}))
/** A ledger object which contains a list of object identifiers.

View File

@@ -293,6 +293,7 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
TYPED_SFIELD(sfProvider, VL, 30)
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
TYPED_SFIELD(sfCredentialType, VL, 32)
TYPED_SFIELD(sfSecureUI, VL, 96)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
TYPED_SFIELD(sfRemarkName, VL, 99)
@@ -391,7 +392,6 @@ UNTYPED_SFIELD(sfGenesisMint, OBJECT, 96)
UNTYPED_SFIELD(sfRemark, OBJECT, 97)
UNTYPED_SFIELD(sfHighReward, OBJECT, 98)
UNTYPED_SFIELD(sfLowReward, OBJECT, 99)
UNTYPED_SFIELD(sfNamedHook, OBJECT, 100)
// array of objects (common)
// ARRAY/1 is reserved for end of array
@@ -428,4 +428,3 @@ UNTYPED_SFIELD(sfImportVLKeys, ARRAY, 94)
UNTYPED_SFIELD(sfActiveValidators, ARRAY, 95)
UNTYPED_SFIELD(sfGenesisMints, ARRAY, 96)
UNTYPED_SFIELD(sfRemarks, ARRAY, 97)
UNTYPED_SFIELD(sfHookNames, ARRAY, 98)

View File

@@ -74,6 +74,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
{sfTickSize, soeOPTIONAL},
{sfNFTokenMinter, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfSecureUI, soeOPTIONAL},
}))
/** This transaction type cancels an existing escrow. */
@@ -500,12 +501,10 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met.
The optional sfHookName / sfHookNames selector(s) it carries come from the common transaction fields. */
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
TRANSACTION(ttCRON, 92, Cron, ({
{sfOwner, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
{sfCron, soeOPTIONAL},
}))
/* Sechedule an alarm for later */

View File

@@ -77,7 +77,6 @@ JSS(HookApiVersion); // field
JSS(HookCanEmit); // field
JSS(HookHash); // field
JSS(HookName); // field
JSS(HookNames); // field
JSS(HookNamespace); // field
JSS(HookOn); // field
JSS(HookOnIncoming); // field
@@ -103,7 +102,6 @@ JSS(FirstLedgerSequence); // in: TransactionSign; field
JSS(LastUpdateTime); // field.
JSS(LimitAmount); // field.
JSS(NetworkID); // field.
JSS(NamedHook); // field.
JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens
JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens
JSS(LPToken); // out: AMM Liquidity Provider tokens info

View File

@@ -108,10 +108,6 @@ InnerObjectFormats::InnerObjectFormats()
{sfAuthorize, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});
add(sfNamedHook.jsonName,
sfNamedHook.getCode(),
{{sfHookName, soeREQUIRED}});
add(sfHookParameter.jsonName,
sfHookParameter.getCode(),
{{sfHookParameterName, soeREQUIRED},

View File

@@ -49,7 +49,6 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookName, soeOPTIONAL},
{sfHookNames, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")

View File

@@ -468,307 +468,6 @@ struct Cron_test : public beast::unit_test::suite
}
}
void
testCronHookName(FeatureBitset features)
{
testcase("cron hook name");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto namesArray = [](std::vector<std::string> const& names) {
Json::Value arr{Json::arrayValue};
for (auto const& n : names)
{
Json::Value e;
e[jss::NamedHook][jss::HookName] = n;
arr.append(e);
}
return arr;
};
// validation + storage of the selector on the cron ledger object
{
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
// single HookName stored on the cron object
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookName] = "41424344";
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close();
auto const accSle = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle && accSle->isFieldPresent(sfCron));
auto const cronSle =
env.le(keylet::child(accSle->getFieldH256(sfCron)));
BEAST_EXPECT(cronSle && cronSle->isFieldPresent(sfHookName));
BEAST_EXPECT(
strHex(cronSle->getFieldVL(sfHookName)) == "41424344");
BEAST_EXPECT(!cronSle->isFieldPresent(sfHookNames));
}
//@@start named-hooks-test-cron-array-storage
// HookNames array stored on the cron object
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookNames] = namesArray({"41424344", "41424345"});
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close();
auto const accSle = env.le(keylet::account(alice.id()));
auto const cronSle =
env.le(keylet::child(accSle->getFieldH256(sfCron)));
BEAST_EXPECT(cronSle && cronSle->isFieldPresent(sfHookNames));
BEAST_EXPECT(cronSle->getFieldArray(sfHookNames).size() == 2);
BEAST_EXPECT(!cronSle->isFieldPresent(sfHookName));
}
//@@end named-hooks-test-cron-array-storage
// empty HookName is accepted as the legacy "unnamed" value, but
// is not persisted as a cron selector
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookName] = "";
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close();
auto const accSle = env.le(keylet::account(alice.id()));
auto const cronSle =
env.le(keylet::child(accSle->getFieldH256(sfCron)));
BEAST_EXPECT(cronSle && !cronSle->isFieldPresent(sfHookName));
BEAST_EXPECT(cronSle && !cronSle->isFieldPresent(sfHookNames));
}
// sfHookName and sfHookNames are mutually exclusive
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookName] = "41424344";
cs[jss::HookNames] = namesArray({"41424345"});
env(cs, fee(XRP(1)), ter(temMALFORMED));
}
// invalid name
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookName] = "DEADBEEF"; // not valid utf-8
env(cs, fee(XRP(1)), ter(temMALFORMED));
}
//@@start named-hooks-test-cron-array-validation
// malformed HookNames arrays are rejected for CronSet too
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookNames] = Json::Value{Json::arrayValue};
env(cs, fee(XRP(1)), ter(temMALFORMED));
}
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
cs[jss::HookNames] = namesArray({"41424344", "41424344"});
env(cs, fee(XRP(1)), ter(temMALFORMED));
}
{
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
Json::Value arr{Json::arrayValue};
Json::Value entry;
entry[sfHook.jsonName][jss::HookName] = "41424344";
arr.append(entry);
cs[jss::HookNames] = arr;
env(cs, fee(XRP(1)), ter(temMALFORMED));
}
//@@end named-hooks-test-cron-array-validation
// a selector may accompany a tfCronUnset: it only acts as a v1
// selector for this transaction's own hook chain, nothing is
// persisted on delete
{
auto cs = cron::set(alice);
cs[jss::HookName] = "41424344";
env(cs, fee(XRP(1)), txflags(tfCronUnset), ter(tesSUCCESS));
env.close();
auto const accSle = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle && !accSle->isFieldPresent(sfCron));
}
}
// without featureNamedHooks both selector forms are rejected outright
// by preflight1
{
Env env{*this, (features | featureCron) - featureNamedHooks};
env.fund(XRP(1000), alice);
env.close();
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
auto csSingle = cron::set(alice);
csSingle[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
csSingle[jss::HookName] = "41424344";
env(csSingle, fee(XRP(1)), ter(temMALFORMED));
auto csArray = cron::set(alice);
csArray[sfStartTime.jsonName] = Json::UInt(baseTime + 1000);
csArray[jss::HookNames] = namesArray({"41424344"});
env(csArray, fee(XRP(1)), ter(temMALFORMED));
}
//@@start named-hooks-test-cron-pseudo-carry
// the emitted Cron pseudo-txn carries the selector
{
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
cs[jss::HookName] = "41424344";
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close();
bool fired = false;
for (int i = 0; i < 40 && !fired; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
{
if (it->first->getTxnType() != ttCRON)
continue;
fired = true;
BEAST_EXPECT(it->first->isFieldPresent(sfHookName));
BEAST_EXPECT(
strHex(it->first->getFieldVL(sfHookName)) ==
"41424344");
}
}
BEAST_EXPECT(fired);
}
//@@end named-hooks-test-cron-pseudo-carry
// a recurring cron keeps its selector across executions
{
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto const baseTime =
env.timeKeeper().now().time_since_epoch().count();
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
cs[sfDelaySeconds.jsonName] = 100;
cs[sfRepeatCount.jsonName] = 3;
cs[jss::HookName] = "41424344";
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close(10s);
// advance until the first execution fires
bool fired = false;
for (int i = 0; i < 40 && !fired; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
if (it->first->getTxnType() == ttCRON)
fired = true;
}
BEAST_EXPECT(fired);
// the recreated cron object still carries the selector
auto const accSle = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle && accSle->isFieldPresent(sfCron));
auto const cronSle =
env.le(keylet::child(accSle->getFieldH256(sfCron)));
BEAST_EXPECT(cronSle && cronSle->isFieldPresent(sfHookName));
BEAST_EXPECT(cronSle && cronSle->getFieldU32(sfRepeatCount) == 2);
if (cronSle && cronSle->isFieldPresent(sfHookName))
BEAST_EXPECT(
strHex(cronSle->getFieldVL(sfHookName)) == "41424344");
}
//@@start named-hooks-test-cron-array-recurrence
// a recurring cron keeps its array selector across executions
{
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto const baseTime =
env.timeKeeper().now().time_since_epoch().count();
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
cs[sfDelaySeconds.jsonName] = 100;
cs[sfRepeatCount.jsonName] = 3;
cs[jss::HookNames] = namesArray({"41424344", "41424345"});
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close(10s);
bool fired = false;
for (int i = 0; i < 40 && !fired; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
if (it->first->getTxnType() == ttCRON)
fired = true;
}
BEAST_EXPECT(fired);
auto const accSle = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle && accSle->isFieldPresent(sfCron));
auto const cronSle =
env.le(keylet::child(accSle->getFieldH256(sfCron)));
BEAST_EXPECT(cronSle && cronSle->isFieldPresent(sfHookNames));
BEAST_EXPECT(cronSle && !cronSle->isFieldPresent(sfHookName));
if (cronSle && cronSle->isFieldPresent(sfHookNames))
{
auto const& hookNames = cronSle->getFieldArray(sfHookNames);
BEAST_EXPECT(hookNames.size() == 2);
BEAST_EXPECT(cronSle->getFieldU32(sfRepeatCount) == 2);
}
}
//@@end named-hooks-test-cron-array-recurrence
// a nameless cron carries no selector (regression)
{
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
auto cs = cron::set(alice);
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close();
bool fired = false;
for (int i = 0; i < 40 && !fired; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
{
if (it->first->getTxnType() != ttCRON)
continue;
fired = true;
BEAST_EXPECT(!it->first->isFieldPresent(sfHookName));
BEAST_EXPECT(!it->first->isFieldPresent(sfHookNames));
}
}
BEAST_EXPECT(fired);
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -779,7 +478,6 @@ struct Cron_test : public beast::unit_test::suite
testDoApply(features);
testCronExecution(features);
testCronHookName(features);
}
public:

View File

@@ -2032,574 +2032,6 @@ public:
}
}
// Build an sfHookNames JSON array from a list of hex name strings:
// [ { "NamedHook": { "HookName": "<hex>" } }, ... ]
static Json::Value
hookNamesArray(std::vector<std::string> const& names)
{
Json::Value arr{Json::arrayValue};
for (auto const& n : names)
{
Json::Value entry;
entry[jss::NamedHook][jss::HookName] = n;
arr.append(entry);
}
return arr;
}
void
testHookNamesArray(FeatureBitset features)
{
testcase("Test hook names array (multiple named hooks per txn)");
using namespace jtx;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const USD = alice["USD"];
bool const namedHooks =
features[featureHooks] && features[featureNamedHooks];
// sfHookNames requires featureNamedHooks
{
Env env{*this, features};
env.fund(XRP(10000), alice);
env.close();
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"41424344"});
env(jv,
M("HookNames requires featureNamedHooks"),
HSFEE,
namedHooks ? ter(tesSUCCESS) : ter(temMALFORMED));
env.close();
}
if (!namedHooks)
return;
// Malformed selector arrays
{
Env env{*this, features};
env.fund(XRP(10000), alice);
env.close();
// sfHookName and sfHookNames are mutually exclusive
{
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "41424344";
jv[jss::HookNames] = hookNamesArray({"41424345"});
env(jv,
M("HookName + HookNames are mutually exclusive"),
HSFEE,
ter(temMALFORMED));
}
// empty array
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = Json::Value{Json::arrayValue};
env(jv, M("empty HookNames"), HSFEE, ter(temMALFORMED));
}
// duplicate names
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"41424344", "41424344"});
env(jv, M("duplicate HookNames"), HSFEE, ter(temMALFORMED));
}
// invalid name (too short)
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"414243"});
env(jv,
M("invalid HookNames entry (too short)"),
HSFEE,
ter(temMALFORMED));
}
// empty name in entry
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({""});
env(jv, M("empty name in HookNames"), HSFEE, ter(temMALFORMED));
}
// entry too long (17 bytes / 34 hex)
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] =
hookNamesArray({"4142434445464748494A4B4C4D4E4F5051"});
env(jv,
M("HookNames entry too long"),
HSFEE,
ter(temMALFORMED));
}
// entry not valid utf-8
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"DEADBEEF"});
env(jv,
M("HookNames entry not utf-8"),
HSFEE,
ter(temMALFORMED));
}
// entry wrapped in the wrong inner object (sfHook can also carry
// sfHookName) rather than the canonical sfNamedHook
{
auto jv = invoke::invoke(alice);
Json::Value arr{Json::arrayValue};
Json::Value entry;
entry[sfHook.jsonName][jss::HookName] = "41424344";
arr.append(entry);
jv[jss::HookNames] = arr;
env(jv,
M("HookNames entry must be a NamedHook"),
HSFEE,
ter(temMALFORMED));
}
// selector-count boundary around maxNamedHookSelectors()
{
// distinct, valid 4-byte (8 hex) ASCII names: "HK??"
auto nthName = [](uint32_t i) {
char const buf[4] = {
'H',
'K',
static_cast<char>('A' + (i / 26)),
static_cast<char>('A' + (i % 26))};
return strHex(std::string(buf, 4));
};
auto buildN = [&](uint32_t n) {
std::vector<std::string> v;
v.reserve(n);
for (uint32_t i = 0; i < n; ++i)
v.push_back(nthName(i));
return hookNamesArray(v);
};
// exactly the max is accepted
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = buildN(hook::maxNamedHookSelectors());
env(jv,
M("max selectors accepted"),
HSFEE,
ter(tesSUCCESS));
env.close();
}
// one over the max is malformed
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] =
buildN(hook::maxNamedHookSelectors() + 1);
env(jv, M("too many selectors"), HSFEE, ter(temMALFORMED));
}
}
}
// Two distinct NAMED hooks, selected individually and together
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
// accept -> named "AAAA" (41414141)
// accept2 -> named "BBBB" (42424242)
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41414141";
jvh[jss::Flags] = hsfCOLLECT;
auto jvh2 = hso(accept2_wasm);
jvh2[jss::HookName] = "42424242";
jvh2[jss::Flags] = hsfCOLLECT;
env(ripple::test::jtx::hook(alice, {{jvh, jvh2}}, 0),
M("Install two distinct named hooks"),
HSFEE);
env.close();
auto execCount = [&]() -> std::size_t {
if (!env.meta()->isFieldPresent(sfHookExecutions))
return 0;
return env.meta()->getFieldArray(sfHookExecutions).size();
};
// No selector: neither named hook fires
{
auto jv = invoke::invoke(alice);
env(jv, M("no selector -> no named hooks"), HSFEE);
env.close();
BEAST_EXPECT(execCount() == 0);
}
//@@start named-hooks-test-array-selection
// Array with one name: only that hook fires
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"41414141"});
env(jv, M("array selects AAAA only"), HSFEE);
env.close();
auto const ex = env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(ex.size() == 1);
BEAST_EXPECT(ex[0].getFieldH256(sfHookHash) == accept_hash);
}
// Array with the other name
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"42424242"});
env(jv, M("array selects BBBB only"), HSFEE);
env.close();
auto const ex = env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(ex.size() == 1);
BEAST_EXPECT(ex[0].getFieldH256(sfHookHash) == accept2_hash);
}
// Array with both names: both hooks fire (in install order)
{
auto jv = invoke::invoke(alice);
jv[jss::HookNames] = hookNamesArray({"41414141", "42424242"});
env(jv, M("array selects both AAAA and BBBB"), HSFEE);
env.close();
auto const ex = env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(ex.size() == 2);
BEAST_EXPECT(ex[0].getFieldH256(sfHookHash) == accept_hash);
BEAST_EXPECT(ex[1].getFieldH256(sfHookHash) == accept2_hash);
}
//@@end named-hooks-test-array-selection
// Single sfHookName still selects exactly one named hook
{
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "42424242";
env(jv, M("single selector still works"), HSFEE);
env.close();
auto const ex = env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(ex.size() == 1);
BEAST_EXPECT(ex[0].getFieldH256(sfHookHash) == accept2_hash);
}
//@@start named-hooks-test-fee-identity
// The fee gate uses the same selector logic as execution, so the
// per-hook cost is additive: fee(both) - fee(none) == the sum of
// each hook's individual incremental cost.
{
auto feeFor = [&](Json::Value const& jv) {
return calculateBaseFee(*env.current(), *env.jt(jv).stx);
};
auto jvNone = invoke::invoke(alice);
auto jvA = invoke::invoke(alice);
jvA[jss::HookNames] = hookNamesArray({"41414141"});
auto jvB = invoke::invoke(alice);
jvB[jss::HookNames] = hookNamesArray({"42424242"});
auto jvBoth = invoke::invoke(alice);
jvBoth[jss::HookNames] =
hookNamesArray({"41414141", "42424242"});
BEAST_EXPECT(feeFor(jvA) > feeFor(jvNone));
BEAST_EXPECT(feeFor(jvB) > feeFor(jvNone));
BEAST_EXPECT(feeFor(jvBoth) > feeFor(jvA));
BEAST_EXPECT(
feeFor(jvBoth) + feeFor(jvNone) ==
feeFor(jvA) + feeFor(jvB));
// a selector matching no installed hook costs the same as no
// selector at all
auto jvNon = invoke::invoke(alice);
jvNon[jss::HookNames] = hookNamesArray({"5A5A5A5A"});
BEAST_EXPECT(feeFor(jvNon) == feeFor(jvNone));
}
//@@end named-hooks-test-fee-identity
// Weak (collect) execution: alice is a weak TSH on bob's trustline.
// The selector gates the weak chain too.
env(fset(alice, asfTshCollect), fee(XRP(1)));
env.close();
{
// selecting only AAAA fires only accept
auto jv = trust(bob, USD(1000));
jv[jss::HookNames] = hookNamesArray({"41414141"});
env(jv, M("weak: array selects AAAA only"), HSFEE);
env.close();
auto const ex = env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(ex.size() == 1);
BEAST_EXPECT(ex[0].getFieldH256(sfHookHash) == accept_hash);
}
{
// selecting both fires both
auto jv = trust(bob, USD(1001));
jv[jss::HookNames] = hookNamesArray({"41414141", "42424242"});
env(jv, M("weak: array selects both"), HSFEE);
env.close();
auto const ex = env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(ex.size() == 2);
BEAST_EXPECT(ex[0].getFieldH256(sfHookHash) == accept_hash);
BEAST_EXPECT(ex[1].getFieldH256(sfHookHash) == accept2_hash);
}
}
}
void
testCronHookName(FeatureBitset features)
{
using namespace jtx;
using namespace std::literals::chrono_literals;
// end-to-end execution needs hooks + named hooks + cron. Guard before
// testcase() so we never open a testcase with no expectations.
if (!(features[featureHooks] && features[featureNamedHooks] &&
features[featureCron]))
return;
testcase("Test cron-targeted named hook execution");
// Set a one-time cron on `owner` carrying `cs` selector fields, advance
// until it fires, and return the hook-execution hashes from the cron
// pseudo-txn's metadata.
auto runCron = [this](Env& env, Account const& owner, Json::Value cs)
-> std::vector<uint256> {
using namespace std::literals::chrono_literals;
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
cs[jss::TransactionType] = jss::CronSet;
cs[jss::Account] = owner.human();
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close();
std::vector<uint256> hashes;
bool fired = false;
for (int i = 0; i < 40 && !fired; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
{
if (it->first->getTxnType() != ttCRON)
continue;
fired = true;
if (it->second &&
it->second->isFieldPresent(sfHookExecutions))
for (auto const& e :
it->second->getFieldArray(sfHookExecutions))
hashes.push_back(e.getFieldH256(sfHookHash));
}
}
BEAST_EXPECT(fired);
return hashes;
};
// A named hook coexisting with an unnamed hook
{
auto const alice = Account{"alice"};
Env env{*this, features};
env.fund(XRP(10000), alice);
env.close();
// accept -> named "AAAA"; accept2 -> unnamed; both collect
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41414141";
jvh[jss::Flags] = hsfCOLLECT;
auto jvh2 = hso(accept2_wasm);
jvh2[jss::Flags] = hsfCOLLECT;
env(ripple::test::jtx::hook(alice, {{jvh, jvh2}}, 0),
M("install named + unnamed cron hooks"),
HSFEE);
env.close();
env(fset(alice, asfTshCollect), fee(XRP(1)));
env.close();
// nameless cron: only the unnamed hook fires (existing behaviour)
{
auto const h = runCron(env, alice, Json::Value{});
BEAST_EXPECT(h.size() == 1);
if (h.size() == 1)
BEAST_EXPECT(h[0] == accept2_hash);
}
// named cron "AAAA": the named hook fires alongside the unnamed one
{
Json::Value cs;
cs[jss::HookName] = "41414141";
auto const h = runCron(env, alice, cs);
BEAST_EXPECT(h.size() == 2);
if (h.size() == 2)
{
BEAST_EXPECT(h[0] == accept_hash);
BEAST_EXPECT(h[1] == accept2_hash);
}
}
// non-matching name: only the unnamed hook fires
{
Json::Value cs;
cs[jss::HookName] = "5A5A5A5A";
auto const h = runCron(env, alice, cs);
BEAST_EXPECT(h.size() == 1);
if (h.size() == 1)
BEAST_EXPECT(h[0] == accept2_hash);
}
}
// A cron carrying an array selects multiple distinct named hooks
{
auto const bob = Account{"bob"};
Env env{*this, features};
env.fund(XRP(10000), bob);
env.close();
// accept -> named "AAAA"; accept2 -> named "BBBB"; both collect
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41414141";
jvh[jss::Flags] = hsfCOLLECT;
auto jvh2 = hso(accept2_wasm);
jvh2[jss::HookName] = "42424242";
jvh2[jss::Flags] = hsfCOLLECT;
env(ripple::test::jtx::hook(bob, {{jvh, jvh2}}, 0),
M("install two named cron hooks"),
HSFEE);
env.close();
env(fset(bob, asfTshCollect), fee(XRP(1)));
env.close();
// array selects both named hooks
{
Json::Value cs;
cs[jss::HookNames] = hookNamesArray({"41414141", "42424242"});
auto const h = runCron(env, bob, cs);
BEAST_EXPECT(h.size() == 2);
if (h.size() == 2)
{
BEAST_EXPECT(h[0] == accept_hash);
BEAST_EXPECT(h[1] == accept2_hash);
}
}
// array selects just one of the two
{
Json::Value cs;
cs[jss::HookNames] = hookNamesArray({"42424242"});
auto const h = runCron(env, bob, cs);
BEAST_EXPECT(h.size() == 1);
if (h.size() == 1)
BEAST_EXPECT(h[0] == accept2_hash);
}
// nameless cron fires neither named hook
{
auto const h = runCron(env, bob, Json::Value{});
BEAST_EXPECT(h.size() == 0);
}
}
// A recurring named cron keeps firing the named hook on every
// recurrence
{
auto const carol = Account{"carol"};
Env env{*this, features};
env.fund(XRP(10000), carol);
env.close();
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41414141";
jvh[jss::Flags] = hsfCOLLECT;
env(ripple::test::jtx::hook(carol, {{jvh}}, 0),
M("install recurring named cron hook"),
HSFEE);
env.close();
env(fset(carol, asfTshCollect), fee(XRP(1)));
env.close();
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
Json::Value cs;
cs[jss::TransactionType] = jss::CronSet;
cs[jss::Account] = carol.human();
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
cs[sfDelaySeconds.jsonName] = 100;
cs[sfRepeatCount.jsonName] = 2; // fires 3 times in total
cs[jss::HookName] = "41414141";
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close(10s);
int firings = 0;
int namedExecutions = 0;
for (int i = 0; i < 100 && firings < 3; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
{
if (it->first->getTxnType() != ttCRON)
continue;
++firings;
if (it->second &&
it->second->isFieldPresent(sfHookExecutions))
for (auto const& e :
it->second->getFieldArray(sfHookExecutions))
if (e.getFieldH256(sfHookHash) == accept_hash)
++namedExecutions;
}
}
// the named hook fired on every recurrence, not just the first --
// i.e. the selector persisted across cron re-creation
BEAST_EXPECT(firings == 3);
BEAST_EXPECT(namedExecutions == 3);
}
//@@start named-hooks-test-recurring-cron-array
// A recurring cron carrying HookNames keeps firing both selected named
// hooks on every recurrence
{
auto const dave = Account{"dave"};
Env env{*this, features};
env.fund(XRP(10000), dave);
env.close();
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41414141";
jvh[jss::Flags] = hsfCOLLECT;
auto jvh2 = hso(accept2_wasm);
jvh2[jss::HookName] = "42424242";
jvh2[jss::Flags] = hsfCOLLECT;
env(ripple::test::jtx::hook(dave, {{jvh, jvh2}}, 0),
M("install recurring named cron hooks"),
HSFEE);
env.close();
env(fset(dave, asfTshCollect), fee(XRP(1)));
env.close();
auto const baseTime =
env.current()->parentCloseTime().time_since_epoch().count();
Json::Value cs;
cs[jss::TransactionType] = jss::CronSet;
cs[jss::Account] = dave.human();
cs[sfStartTime.jsonName] = Json::UInt(baseTime + 100);
cs[sfDelaySeconds.jsonName] = 100;
cs[sfRepeatCount.jsonName] = 2; // fires 3 times in total
cs[jss::HookNames] = hookNamesArray({"41414141", "42424242"});
env(cs, fee(XRP(1)), ter(tesSUCCESS));
env.close(10s);
int firings = 0;
int firstNamedExecutions = 0;
int secondNamedExecutions = 0;
for (int i = 0; i < 100 && firings < 3; ++i)
{
env.close(10s);
auto const& txs = env.closed()->txs;
for (auto it = txs.begin(); it != txs.end(); ++it)
{
if (it->first->getTxnType() != ttCRON)
continue;
++firings;
if (it->second &&
it->second->isFieldPresent(sfHookExecutions))
for (auto const& e :
it->second->getFieldArray(sfHookExecutions))
{
if (e.getFieldH256(sfHookHash) == accept_hash)
++firstNamedExecutions;
if (e.getFieldH256(sfHookHash) == accept2_hash)
++secondNamedExecutions;
}
}
}
BEAST_EXPECT(firings == 3);
BEAST_EXPECT(firstNamedExecutions == 3);
BEAST_EXPECT(secondNamedExecutions == 3);
}
//@@end named-hooks-test-recurring-cron-array
}
void
testFillCopy(FeatureBitset features)
{
@@ -15667,8 +15099,6 @@ public:
testHookOnV2(features);
testHookName(features);
testHookNamesArray(features);
testCronHookName(features);
testFillCopy(features);

View File

@@ -1510,13 +1510,7 @@ TxQ::accept(Application& app, OpenView& view)
uint256 klStart = keylet::cron(0, accountID).key;
uint256 const klEnd = keylet::cron(currentTime + 1, accountID).key;
bool const namedHooks = view.rules().enabled(featureNamedHooks);
//@@start named-hooks-cron-scan-auth
// owner account -> its cron ledger object (there is at most one cron
// per account). The SLE is retained so any named-hook selector(s) it
// carries can be copied onto the emitted Cron pseudo-txn.
std::map<AccountID, std::shared_ptr<SLE const>> cronAccs;
std::set<AccountID> cronAccs;
auto counter = 0;
// include max 128 cron txns in the ledger
@@ -1534,70 +1528,23 @@ TxQ::accept(Application& app, OpenView& view)
if (safe_cast<LedgerEntryType>(
sle->getFieldU16(sfLedgerEntryType)) == ltCRON)
{
AccountID const owner = sle->getAccountID(sfOwner);
if (!namedHooks)
{
// Preserve the pre-NamedHooks Cron scheduler output:
// every due ltCRON owner is pinged, even if a stale or
// orphan object is encountered in the scan window.
cronAccs.emplace(owner, sle);
}
else
{
// Only the account's current cron pointer is
// authoritative once the selector amendment needs the
// cron object for pseudo-txn field copying.
// Stale/orphan entries still consume this scan loop's
// 128-key budget; changing that accounting is a broader
// Cron scheduler behavior change and is intentionally
// left out here.
auto const accountSle =
view.read(keylet::account(owner));
if (accountSle && accountSle->isFieldPresent(sfCron) &&
accountSle->getFieldH256(sfCron) == sle->key())
{
cronAccs.emplace(owner, sle);
}
}
// valid cron object, add it to the list
cronAccs.emplace(sle->getAccountID(sfOwner));
}
}
klStart = *next;
}
//@@end named-hooks-cron-scan-auth
auto const seq = view.info().seq;
// insert Cron pseudos for each of the accs we need to ping
for (auto const& cronAcc : cronAccs)
for (AccountID const& id : cronAccs)
{
AccountID const& id = cronAcc.first;
std::shared_ptr<SLE const> const& cronSle = cronAcc.second;
STTx cronTx(ttCRON, [&](auto& obj) {
STTx cronTx(ttCRON, [=](auto& obj) {
obj[sfAccount] = AccountID();
obj[sfLedgerSequence] = seq;
obj[sfOwner] = id;
//@@start named-hooks-cron-pseudo-selector
if (namedHooks)
{
// Bind the pseudo-txn to the exact cron object found during
// TxQ scheduling. Cron::doApply treats this as stale if the
// owner replaced or removed the cron before the pseudo
// applies.
obj[sfCron] = cronSle->key();
// carry the cron's named-hook selector(s), if any, so the
// pseudo-txn triggers the selected named hook(s) on the
// owner (in addition to the owner's unnamed hooks, as
// always)
if (cronSle->isFieldPresent(sfHookName))
obj.setFieldVL(
sfHookName, cronSle->getFieldVL(sfHookName));
if (cronSle->isFieldPresent(sfHookNames))
obj.setFieldArray(
sfHookNames, cronSle->getFieldArray(sfHookNames));
}
//@@end named-hooks-cron-pseudo-selector
});
uint256 txID = cronTx.getTransactionID();

View File

@@ -45,14 +45,6 @@ Change::preflight(PreflightContext const& ctx)
if (!isTesSuccess(ret))
return ret;
if (ctx.tx.isFieldPresent(sfHookNames) ||
(ctx.rules.enabled(featureNamedHooks) &&
ctx.tx.isFieldPresent(sfHookName)))
{
JLOG(ctx.j.warn()) << "Change: Hook selectors are not allowed";
return temMALFORMED;
}
auto account = ctx.tx.getAccountID(sfAccount);
if (account != beast::zero)
{

View File

@@ -27,7 +27,6 @@
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
#include <optional>
namespace ripple {
@@ -47,14 +46,6 @@ Cron::preflight(PreflightContext const& ctx)
if (!isTesSuccess(ret))
return ret;
bool const namedHooks = ctx.rules.enabled(featureNamedHooks);
//@@start named-hooks-cron-preflight-selector
auto const selectors = validateHookSelectors(ctx, true);
if (!isTesSuccess(selectors))
return selectors;
//@@end named-hooks-cron-preflight-selector
auto account = ctx.tx.getAccountID(sfAccount);
if (account != beast::zero)
{
@@ -84,16 +75,6 @@ Cron::preflight(PreflightContext const& ctx)
return temBAD_SEQUENCE;
}
//@@start named-hooks-cron-preflight-binding
if (namedHooks &&
(!ctx.tx.isFieldPresent(sfCron) ||
ctx.tx.getFieldH256(sfCron) == beast::zero))
{
JLOG(ctx.j.warn()) << "Cron: Missing cron object";
return temMALFORMED;
}
//@@end named-hooks-cron-preflight-binding
return tesSUCCESS;
}
@@ -111,7 +92,6 @@ Cron::doApply()
{
auto& view = ctx_.view();
auto const& tx = ctx_.tx;
bool const namedHooks = view.rules().enabled(featureNamedHooks);
if (view.rules().enabled(fixCronStacking))
{
@@ -135,30 +115,11 @@ Cron::doApply()
if (!sle->isFieldPresent(sfCron))
{
if (namedHooks && tx.isFieldPresent(sfCron))
{
JLOG(j_.warn()) << "Cron: stale cron pseudo for account " << id;
return tesSUCCESS;
}
JLOG(j_.warn()) << "Cron: sfCron missing from account " << id;
return tefINTERNAL;
}
//@@start named-hooks-cron-stale-pseudo
uint256 ptr = sle->getFieldH256(sfCron);
// The Cron pseudo-txn is scheduled against a specific ltCRON object. If
// the owner replaced or removed that object earlier in this ledger, this
// pseudo is stale and must not execute the owner's new cron with the old
// pseudo's selector fields.
if (namedHooks && tx.isFieldPresent(sfCron) &&
ptr != tx.getFieldH256(sfCron))
{
JLOG(j_.warn()) << "Cron: stale cron pseudo for account " << id;
return tesSUCCESS;
}
//@@end named-hooks-cron-stale-pseudo
Keylet klOld{ltCRON, ptr};
auto sleCron = view.peek(klOld);
if (!sleCron)
@@ -172,20 +133,6 @@ Cron::doApply()
uint32_t lastStartTime = sleCron->getFieldU32(sfStartTime);
//@@start named-hooks-cron-capture-selector
// capture the named-hook selector(s) (if any) before the object is erased,
// so they persist across recurring executions (featureNamedHooks)
std::optional<Blob> hookName;
std::optional<STArray> hookNames;
if (namedHooks)
{
if (sleCron->isFieldPresent(sfHookName))
hookName = sleCron->getFieldVL(sfHookName);
if (sleCron->isFieldPresent(sfHookNames))
hookNames = sleCron->getFieldArray(sfHookNames);
}
//@@end named-hooks-cron-capture-selector
// do all this sanity checking before we modify the ledger...
uint32_t afterTime = lastStartTime + delay;
if (afterTime < lastStartTime)
@@ -229,14 +176,6 @@ Cron::doApply()
sleCron->setFieldU32(sfStartTime, afterTime);
sleCron->setAccountID(sfOwner, id);
//@@start named-hooks-cron-carry-forward
// carry the named-hook selector(s) forward to the next execution
if (hookName)
sleCron->setFieldVL(sfHookName, *hookName);
if (hookNames)
sleCron->setFieldArray(sfHookNames, *hookNames);
//@@end named-hooks-cron-carry-forward
sle->setFieldH256(sfCron, klCron.key);
view.insert(sleCron);

View File

@@ -64,16 +64,6 @@ CronSet::preflight(PreflightContext const& ctx)
bool const hasRepeat = tx.isFieldPresent(sfRepeatCount);
bool const hasStartTime = tx.isFieldPresent(sfStartTime);
//@@start named-hooks-cronset-selector-note
// Note: sfHookName / sfHookNames are common transaction selectors,
// validated by preflight1 (which requires featureNamedHooks). On a CronSet
// they additionally serve as the selector persisted on the cron object (and
// carried by the emitted Cron pseudo-txn), gated in doApply. They are
// deliberately NOT rejected here: a CronSet (or a tfCronUnset) may carry
// sfHookName purely to select named hooks on its own hook chain, exactly as
// for any other transaction.
//@@end named-hooks-cronset-selector-note
if (tx.isFlag(tfCronUnset))
{
// delete operation
@@ -278,23 +268,6 @@ CronSet::doApply()
sleCron->setFieldU32(sfRepeatCount, recur);
sleCron->setAccountID(sfOwner, id);
//@@start named-hooks-cronset-persist-selector
// persist the optional named-hook selector(s) on the cron so the emitted
// Cron pseudo-txn triggers the selected named hook(s) on the owner
// (featureNamedHooks).
if (view.rules().enabled(featureNamedHooks))
{
if (tx.isFieldPresent(sfHookName))
{
Blob const hookName = tx.getFieldVL(sfHookName);
if (!hookName.empty())
sleCron->setFieldVL(sfHookName, hookName);
}
if (tx.isFieldPresent(sfHookNames))
sleCron->setFieldArray(sfHookNames, tx.getFieldArray(sfHookNames));
}
//@@end named-hooks-cronset-persist-selector
sle->setFieldH256(sfCron, klCron.key);
view.update(sle);

View File

@@ -197,6 +197,31 @@ SetAccount::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (tx.isFieldPresent(sfSecureUI))
{
if (!ctx.rules.enabled(featureSecureUI))
return temMALFORMED;
Blob ui = tx.getFieldVL(sfSecureUI);
if (ui.size() == 0)
{
// this is an unset operation, pass
}
else if (ui.size() > 4096)
{
JLOG(j.trace()) << "SecureUI: Too long > 4096 bytes";
return temMALFORMED;
}
else if (!URIToken::validateUTF8(ui))
{
JLOG(j.trace()) << "SecureUI: Not UTF-8";
return temMALFORMED;
}
// valid
}
return preflight2(ctx);
}
@@ -699,6 +724,23 @@ SetAccount::doApply()
sle->setFieldU16(sfHookStateScale, newScale);
}
}
if (tx.isFieldPresent(sfSecureUI))
{
Blob ui = tx.getFieldVL(sfSecureUI);
if (ui.size() == 0)
{
// unset operation
if (sle->isFieldPresent(sfSecureUI))
sle->makeFieldAbsent(sfSecureUI);
}
else
{
// set operation
sle->setFieldVL(sfSecureUI, std::move(ui));
}
}
ctx_.view().update(sle);
return tesSUCCESS;

View File

@@ -36,65 +36,15 @@
#include <xrpl/hook/Enum.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/UintTypes.h>
#include <limits>
#include <optional>
#include <set>
namespace ripple {
// Determines whether `hookObj` (a hook installed on an account's hook chain) is
// selected to run by the triggering transaction `tx`, according to the
// transaction's hook-name selector(s) (featureNamedHooks).
//
// * A hook with no name (absent or empty sfHookName) is always selected.
// * A named hook is selected when the transaction carries a matching
// selector: either a single sfHookName equal to the hook's name, or an
// sfHookNames array containing an entry equal to it. The two forms are
// mutually exclusive at preflight, so the array is consulted first and,
// when present, is authoritative.
//
// No explicit amendment check is needed here: a hook can only carry a name when
// featureNamedHooks is enabled (SetHook enforces this and amendments do not
// deactivate), so for any hook with a non-empty name the amendment is
// necessarily active. Pre-amendment no hook is named, so this function returns
// true for every hook and the selector fields are inert. Validation of selector
// fields on emitted/pseudo transactions is handled on their preflight paths.
//
// This mirrors the gate applied during fee calculation so that the fee charged
// always matches the set of hooks that actually execute.
static bool
txSelectsHook(STObject const& hookObj, STTx const& tx)
{
if (!hookObj.isFieldPresent(sfHookName))
return true;
Blob const required = hookObj.getFieldVL(sfHookName);
if (required.empty())
return true;
if (tx.isFieldPresent(sfHookNames))
{
for (auto const& entry : tx.getFieldArray(sfHookNames))
{
if (entry.getFName() == sfNamedHook &&
entry.isFieldPresent(sfHookName) &&
entry.getFieldVL(sfHookName) == required)
return true;
}
return false;
}
if (tx.isFieldPresent(sfHookName))
return tx.getFieldVL(sfHookName) == required;
return false;
}
/** Performs early sanity checks on the txid */
NotTEC
preflight0(PreflightContext const& ctx)
@@ -140,70 +90,6 @@ preflight0(PreflightContext const& ctx)
return tesSUCCESS;
}
NotTEC
validateHookSelectors(PreflightContext const& ctx, bool allowLegacyHookName)
{
if (ctx.tx.isFieldPresent(sfHookName))
{
if (!ctx.rules.enabled(featureHooks))
return temMALFORMED;
if (!ctx.rules.enabled(featureNamedHooks))
{
// Some pre-amendment paths (notably emitted transactions and
// pseudo-transactions) historically accepted the legacy single
// sfHookName field because they did not run this validation.
// Preserve that behavior where callers opt in. The new sfHookNames
// array remains rejected below unless featureNamedHooks is enabled.
if (!allowLegacyHookName)
return temMALFORMED;
}
if (ctx.rules.enabled(featureNamedHooks) &&
!SetHook::validateHookName(ctx.tx.getFieldVL(sfHookName), ctx.j))
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfHookNames))
{
if (!ctx.rules.enabled(featureHooks) ||
!ctx.rules.enabled(featureNamedHooks))
return temMALFORMED;
// sfHookName (single selector) and sfHookNames (array selector) are
// mutually exclusive
if (ctx.tx.isFieldPresent(sfHookName))
return temMALFORMED;
auto const& names = ctx.tx.getFieldArray(sfHookNames);
if (names.empty() || names.size() > hook::maxNamedHookSelectors())
return temMALFORMED;
std::set<Blob> seen;
for (auto const& entry : names)
{
// each entry must be a canonical sfNamedHook wrapper, not some
// other inner object that merely happens to carry sfHookName
if (entry.getFName() != sfNamedHook)
return temMALFORMED;
if (!entry.isFieldPresent(sfHookName))
return temMALFORMED;
Blob const name = entry.getFieldVL(sfHookName);
// an empty name is "unnamed" and is meaningless as a selector
if (name.empty() || !SetHook::validateHookName(name, ctx.j))
return temMALFORMED;
if (!seen.insert(name).second)
return temMALFORMED; // duplicate selector
}
}
return tesSUCCESS;
}
/** Performs early sanity checks on the account and fee fields */
NotTEC
preflight1(PreflightContext const& ctx)
@@ -235,22 +121,14 @@ preflight1(PreflightContext const& ctx)
return temBAD_FEE;
}
bool const emittedBypass = ctx.rules.enabled(featureHooks) &&
hook::isEmittedTxn(ctx.tx) &&
((ctx.app.getHashRouter().getFlags(ctx.tx.getTransactionID()) &
SF_EMITTED) ||
(ctx.flags & tapPREFLIGHT_EMIT));
auto const selectors = validateHookSelectors(ctx, emittedBypass);
if (!isTesSuccess(selectors))
return selectors;
// if a hook emitted this transaction we bypass signature checks
// there is a bar to circularing emitted transactions on the network
// in their prevalidated form so this is safe
if (ctx.rules.enabled(featureHooks) && hook::isEmittedTxn(ctx.tx))
{
if (emittedBypass)
if ((ctx.app.getHashRouter().getFlags(ctx.tx.getTransactionID()) &
SF_EMITTED) ||
(ctx.flags & tapPREFLIGHT_EMIT))
{
if (ctx.tx.getSeqProxy().isTicket() &&
ctx.tx.isFieldPresent(sfAccountTxnID))
@@ -271,6 +149,16 @@ preflight1(PreflightContext const& ctx)
}
}
if (ctx.tx.isFieldPresent(sfHookName))
{
if (!ctx.rules.enabled(featureHooks) ||
!ctx.rules.enabled(featureNamedHooks))
return temMALFORMED;
if (!SetHook::validateHookName(ctx.tx.getFieldVL(sfHookName), ctx.j))
return temMALFORMED;
}
auto const spk = ctx.tx.getSigningPubKey();
if (!spk.empty() && !publicKeyType(makeSlice(spk)))
@@ -395,11 +283,19 @@ Transactor::calculateHookChainFee(
// LCOV_EXCL_STOP
}
//@@start named-hooks-fee-gate
// skip hooks not selected by the transaction's hook-name selector(s)
if (!txSelectsHook(hookObj, tx))
continue;
//@@end named-hooks-fee-gate
std::optional<Blob> requiredHookName;
if (hookObj.isFieldPresent(sfHookName) &&
hookObj.getFieldVL(sfHookName).size() > 0)
requiredHookName = hookObj.getFieldVL(sfHookName);
if (requiredHookName)
{
// need to specify same hook name in the transaction
if (!tx.isFieldPresent(sfHookName))
continue;
if (*requiredHookName != tx.getFieldVL(sfHookName))
continue;
}
uint32_t flags = 0;
if (hookObj.isFieldPresent(sfFlags))
@@ -1458,11 +1354,19 @@ Transactor::executeHookChain(
// LCOV_EXCL_STOP
}
//@@start named-hooks-execution-gate
// skip hooks not selected by the transaction's hook-name selector(s)
if (!txSelectsHook(hookObj, ctx_.tx))
continue;
//@@end named-hooks-execution-gate
std::optional<Blob> requiredHookName;
if (hookObj.isFieldPresent(sfHookName) &&
hookObj.getFieldVL(sfHookName).size() > 0)
requiredHookName = hookObj.getFieldVL(sfHookName);
if (requiredHookName)
{
// need to specify same hook name in the transaction
if (!ctx_.tx.isFieldPresent(sfHookName))
continue;
if (*requiredHookName != ctx_.tx.getFieldVL(sfHookName))
continue;
}
// check if the hook can fire
uint256 hookOn = hook::getHookOn(
@@ -1578,45 +1482,6 @@ Transactor::executeHookChain(
return tesSUCCESS;
}
static std::optional<uint8_t>
callbackHookChainPosition(
STObject const& emitDetails,
AccountID const& callbackAccountID,
uint256 const& callbackHookHash)
{
if (!emitDetails.isFieldPresent(sfEmitNonce) ||
!emitDetails.isFieldPresent(sfEmitParentTxnID))
return std::nullopt;
auto const expectedNonce = emitDetails.getFieldH256(sfEmitNonce);
auto const parentTxnID = emitDetails.getFieldH256(sfEmitParentTxnID);
for (uint8_t position = 0; position < hook::maxHookChainLength();
++position)
{
// etxn_nonce hashes the uint16_t emit_nonce_counter; hash_append
// includes integral width, so a uint32_t with the same value will not
// match.
for (uint16_t nonce = 0; nonce <= hook_api::max_nonce; ++nonce)
{
for (uint32_t flags = 0; flags < 4; ++flags)
{
auto const candidate = sha512Half(
HashPrefix::emitTxnNonce,
parentTxnID,
nonce,
callbackAccountID,
callbackHookHash,
(static_cast<uint32_t>(position) << 2U) | flags);
if (candidate == expectedNonce)
return position;
}
}
}
return std::nullopt;
}
void
Transactor::doHookCallback(
std::shared_ptr<STObject const> const& provisionalMeta)
@@ -1636,21 +1501,6 @@ Transactor::doHookCallback(
AccountID const& callbackAccountID =
emitDetails.getAccountID(sfEmitCallback);
uint256 const& callbackHookHash = emitDetails.getFieldH256(sfEmitHookHash);
bool const namedHooks = view().rules().enabled(featureNamedHooks);
bool const hasCallbackNonce = namedHooks &&
emitDetails.isFieldPresent(sfEmitNonce) &&
emitDetails.isFieldPresent(sfEmitParentTxnID);
std::optional<uint8_t> callbackPosition;
if (hasCallbackNonce)
callbackPosition = callbackHookChainPosition(
emitDetails, callbackAccountID, callbackHookHash);
if (hasCallbackNonce && !callbackPosition)
{
JLOG(j_.warn()) << "HookError[" << callbackAccountID
<< "]: Callback nonce did not identify hook slot";
return;
}
auto const& hooksCallback = view().peek(keylet::hook(callbackAccountID));
auto const& hookDef = view().peek(keylet::hookDefinition(callbackHookHash));
@@ -1682,17 +1532,6 @@ Transactor::doHookCallback(
bool found = false;
auto const& hooks = hooksCallback->getFieldArray(sfHooks);
// Count how many slots currently carry the callback hash. The emit-position
// filter below only needs to disambiguate genuinely duplicate-hash slots;
// when exactly one slot matches we deliver to it even if the emitting hook
// was relocated to a different chain index since it emitted.
std::size_t callbackHashSlots = 0;
for (auto const& hookObj : hooks)
if (hookObj.isFieldPresent(sfHookHash) &&
hookObj.getFieldH256(sfHookHash) == callbackHookHash)
++callbackHashSlots;
uint8_t hook_no = 0;
for (auto const& hookObj : hooks)
{
@@ -1704,15 +1543,6 @@ Transactor::doHookCallback(
if (hookObj.getFieldH256(sfHookHash) != callbackHookHash)
continue;
//@@start named-hooks-callback-slot-filter
// Only disambiguate by emit position when more than one slot carries
// the hash; an unambiguous callback is otherwise delivered even after a
// benign hook relocation within this transaction.
if (namedHooks && callbackPosition && callbackHashSlots > 1 &&
*callbackPosition != hook_no - 1)
continue;
//@@end named-hooks-callback-slot-filter
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
// fetch the namespace either from the hook object of, if absent, the
@@ -1983,7 +1813,7 @@ Transactor::doTSH(
void
Transactor::doAgainAsWeak(
AccountID const& hookAccountID,
std::set<std::pair<uint8_t, uint256>> const& requestedHooks,
std::set<uint256> const& hookHashes,
hook::HookStateMap& stateMap,
std::vector<hook::HookResult>& results,
std::shared_ptr<STObject const> const& provisionalMeta)
@@ -2003,7 +1833,6 @@ Transactor::doAgainAsWeak(
}
auto const& hooks = hooksArray->getFieldArray(sfHooks);
bool const namedHooks = view().rules().enabled(featureNamedHooks);
uint8_t hook_no = 0;
for (auto const& hookObj : hooks)
{
@@ -2014,58 +1843,9 @@ Transactor::doAgainAsWeak(
uint256 const& hookHash = hookObj.getFieldH256(sfHookHash);
//@@start named-hooks-aaw-exact-replay
if (namedHooks)
{
bool matched = requestedHooks.find({hook_no - 1, hookHash}) !=
requestedHooks.end();
if (!matched)
{
// Exact {position, hash} failed: fall back to hash matching
// only when this hash is unambiguous in both the requested set
// and the current chain (a single requesting slot whose index
// merely shifted within this same transaction). Ambiguous
// duplicate-hash cases still require the exact position.
std::size_t reqForHash = 0;
for (auto const& requestedHook : requestedHooks)
if (requestedHook.second == hookHash)
++reqForHash;
std::size_t curForHash = 0;
for (auto const& other : hooks)
if (other.isFieldPresent(sfHookHash) &&
other.getFieldH256(sfHookHash) == hookHash)
++curForHash;
if (reqForHash == 1 && curForHash == 1)
matched = true;
}
if (!matched)
continue;
}
else
{
bool requestedHash = false;
for (auto const& requestedHook : requestedHooks)
{
if (requestedHook.second == hookHash)
{
requestedHash = true;
break;
}
}
if (!requestedHash)
continue;
}
//@@end named-hooks-aaw-exact-replay
//@@start named-hooks-aaw-selector-gate
// honor the transaction's hook-name selector(s) on replay too, so a
// selected hook's "execute again as weak" request cannot replay an
// unselected slot that merely shares the same hook hash under a
// different name (featureNamedHooks; inert pre-amendment as no hook can
// be named)
if (namedHooks && !txSelectsHook(hookObj, ctx_.tx))
if (hookHashes.find(hookObj.getFieldH256(sfHookHash)) ==
hookHashes.end())
continue;
//@@end named-hooks-aaw-selector-gate
auto const& hookDef = view().peek(keylet::hookDefinition(hookHash));
if (!hookDef)
@@ -2187,11 +1967,10 @@ Transactor::operator()()
bool const hooksEnabled = view().rules().enabled(featureHooks);
// AgainAsWeak records the exact hook slot and hash that requested replay so
// featureNamedHooks can disambiguate duplicate-hash slots. Before that
// amendment is enabled, doAgainAsWeak deliberately falls back to the
// historical hash-only replay rule.
std::map<AccountID, std::set<std::pair<uint8_t, uint256>>> aawMap;
// AgainAsWeak map stores information about accounts whose strongly executed
// hooks request an additional weak execution after the otxn has finished
// application to the ledger
std::map<AccountID, std::set<uint256>> aawMap;
std::vector<std::pair<AccountID, bool>> tsh =
hook::getTransactionalStakeHolders(ctx_.tx, ctx_.view());
@@ -2248,11 +2027,9 @@ Transactor::operator()()
if (hookResult.executeAgainAsWeak)
{
if (aawMap.find(hookResult.account) == aawMap.end())
aawMap[hookResult.account] = {
{hookResult.hookChainPosition, hookResult.hookHash}};
aawMap[hookResult.account] = {hookResult.hookHash};
else
aawMap[hookResult.account].emplace(
hookResult.hookChainPosition, hookResult.hookHash);
aawMap[hookResult.account].emplace(hookResult.hookHash);
}
}
}
@@ -2613,9 +2390,8 @@ Transactor::operator()()
doTSH(false, tsh, stateMap, weakResults, proMeta);
// execute any hooks that nominated for 'again as weak'
for (auto const& [accID, requestedHooks] : aawMap)
doAgainAsWeak(
accID, requestedHooks, stateMap, weakResults, proMeta);
for (auto const& [accID, hookHashes] : aawMap)
doAgainAsWeak(accID, hookHashes, stateMap, weakResults, proMeta);
// write hook results
hook::finalizeHookState(stateMap, ctx_, ctx_.tx.getTransactionID());

View File

@@ -27,8 +27,6 @@
#include <xrpld/ledger/detail/ApplyViewBase.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/XRPAmount.h>
#include <set>
#include <utility>
#include <variant>
namespace ripple {
@@ -202,7 +200,7 @@ protected:
void
doAgainAsWeak(
AccountID const& hookAccountID,
std::set<std::pair<uint8_t, uint256>> const& requestedHooks,
std::set<uint256> const& hookHashes,
hook::HookStateMap& stateMap,
std::vector<hook::HookResult>& results,
std::shared_ptr<STObject const> const& provisionalMeta);
@@ -279,11 +277,6 @@ private:
NotTEC
preflight0(PreflightContext const& ctx);
NotTEC
validateHookSelectors(
PreflightContext const& ctx,
bool allowLegacyHookName = false);
/** Performs early sanity checks on the account and fee fields */
NotTEC
preflight1(PreflightContext const& ctx);