mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-11 17:20:08 +00:00
Compare commits
1 Commits
chrische-n
...
SecureUI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f8a9c9ac |
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -108,10 +108,6 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfAuthorize, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL}});
|
||||
|
||||
add(sfNamedHook.jsonName,
|
||||
sfNamedHook.getCode(),
|
||||
{{sfHookName, soeREQUIRED}});
|
||||
|
||||
add(sfHookParameter.jsonName,
|
||||
sfHookParameter.getCode(),
|
||||
{{sfHookParameterName, soeREQUIRED},
|
||||
|
||||
@@ -49,7 +49,6 @@ TxFormats::TxFormats()
|
||||
{sfNetworkID, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfHookName, soeOPTIONAL},
|
||||
{sfHookNames, soeOPTIONAL},
|
||||
};
|
||||
|
||||
#pragma push_macro("UNWRAP")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user