ExtendedHookState (#406)

This commit is contained in:
tequ
2025-10-23 17:57:38 +09:00
committed by GitHub
parent 96222baf5e
commit 6f148a8ac7
17 changed files with 1490 additions and 285 deletions

View File

@@ -15,6 +15,7 @@
#define sfHookEmitCount ((1U << 16U) + 18U) #define sfHookEmitCount ((1U << 16U) + 18U)
#define sfHookExecutionIndex ((1U << 16U) + 19U) #define sfHookExecutionIndex ((1U << 16U) + 19U)
#define sfHookApiVersion ((1U << 16U) + 20U) #define sfHookApiVersion ((1U << 16U) + 20U)
#define sfHookStateScale ((1U << 16U) + 21U)
#define sfNetworkID ((2U << 16U) + 1U) #define sfNetworkID ((2U << 16U) + 1U)
#define sfFlags ((2U << 16U) + 2U) #define sfFlags ((2U << 16U) + 2U)
#define sfSourceTag ((2U << 16U) + 3U) #define sfSourceTag ((2U << 16U) + 3U)

View File

@@ -29,6 +29,8 @@ enum HookEmissionFlags : uint16_t {
}; };
} // namespace ripple } // namespace ripple
using namespace ripple;
namespace hook { namespace hook {
// RH TODO: put these somewhere better, and allow rules to be fed in // RH TODO: put these somewhere better, and allow rules to be fed in
inline uint32_t inline uint32_t
@@ -43,11 +45,27 @@ maxHookParameterValueSize(void)
return 256; return 256;
} }
inline uint32_t inline uint16_t
maxHookStateDataSize(void) maxHookStateScale(void)
{ {
return 16;
}
inline uint32_t
maxHookStateDataSize(uint16_t hookStateScale)
{
if (hookStateScale == 0)
{
// should not happen, but just in case
return 256U; return 256U;
} }
if (hookStateScale > maxHookStateScale())
{
// should not happen, but just in case
return 256 * maxHookStateScale();
}
return 256U * hookStateScale;
}
inline uint32_t inline uint32_t
maxHookWasmSize(void) maxHookWasmSize(void)

View File

@@ -32,6 +32,7 @@ class HookStateMap : public std::map<
std::tuple< std::tuple<
int64_t, // remaining available ownercount int64_t, // remaining available ownercount
int64_t, // total namespace count int64_t, // total namespace count
uint16_t, // hook state scale
std::map< std::map<
ripple::uint256, // namespace ripple::uint256, // namespace
std::map< std::map<
@@ -465,9 +466,6 @@ apply(
struct HookContext; struct HookContext;
uint32_t
computeHookStateOwnerCount(uint32_t hookStateCount);
int64_t int64_t
computeExecutionFee(uint64_t instructionCount); computeExecutionFee(uint64_t instructionCount);
int64_t int64_t

View File

@@ -862,12 +862,6 @@ parseCurrency(uint8_t* cu_ptr, uint32_t cu_len)
return {}; return {};
} }
uint32_t
hook::computeHookStateOwnerCount(uint32_t hookStateCount)
{
return hookStateCount;
}
inline int64_t inline int64_t
serialize_keylet( serialize_keylet(
ripple::Keylet& kl, ripple::Keylet& kl,
@@ -1080,14 +1074,18 @@ hook::setHookState(
return tefINTERNAL; return tefINTERNAL;
// if the blob is too large don't set it // if the blob is too large don't set it
if (data.size() > hook::maxHookStateDataSize()) uint16_t const hookStateScale = sleAccount->isFieldPresent(sfHookStateScale)
? sleAccount->getFieldU16(sfHookStateScale)
: 1;
if (data.size() > hook::maxHookStateDataSize(hookStateScale))
return temHOOK_DATA_TOO_LARGE; return temHOOK_DATA_TOO_LARGE;
auto hookStateKeylet = ripple::keylet::hookState(acc, key, ns); auto hookStateKeylet = ripple::keylet::hookState(acc, key, ns);
auto hookStateDirKeylet = ripple::keylet::hookStateDir(acc, ns); auto hookStateDirKeylet = ripple::keylet::hookStateDir(acc, ns);
uint32_t stateCount = sleAccount->getFieldU32(sfHookStateCount); uint32_t stateCount = sleAccount->getFieldU32(sfHookStateCount);
uint32_t oldStateReserve = computeHookStateOwnerCount(stateCount); uint32_t oldStateCount = stateCount;
auto hookState = view.peek(hookStateKeylet); auto hookState = view.peek(hookStateKeylet);
@@ -1118,12 +1116,14 @@ hook::setHookState(
if (stateCount > 0) if (stateCount > 0)
--stateCount; // guard this because in the "impossible" event it is --stateCount; // guard this because in the "impossible" event it is
// already 0 we'll wrap back to int_max // already 0 we'll wrap back to int_max
// if removing this state entry would destroy the allotment then reduce // if removing this state entry would destroy the allotment then reduce
// the owner count // the owner count
if (computeHookStateOwnerCount(stateCount) < oldStateReserve) if (stateCount < oldStateCount)
adjustOwnerCount(view, sleAccount, -1, j); adjustOwnerCount(view, sleAccount, -hookStateScale, j);
if (view.rules().enabled(featureExtendedHookState) && stateCount == 0)
sleAccount->makeFieldAbsent(sfHookStateCount);
else
sleAccount->setFieldU32(sfHookStateCount, stateCount); sleAccount->setFieldU32(sfHookStateCount, stateCount);
if (nsDestroyed) if (nsDestroyed)
@@ -1151,19 +1151,19 @@ hook::setHookState(
{ {
++stateCount; ++stateCount;
if (computeHookStateOwnerCount(stateCount) > oldStateReserve) if (stateCount > oldStateCount)
{ {
// the hook used its allocated allotment of state entries for its // the hook used its allocated allotment of state entries for its
// previous ownercount increment ownercount and give it another // previous ownercount increment ownercount and give it another
// allotment // allotment
++ownerCount; ownerCount += hookStateScale;
XRPAmount const newReserve{view.fees().accountReserve(ownerCount)}; XRPAmount const newReserve{view.fees().accountReserve(ownerCount)};
if (STAmount((*sleAccount)[sfBalance]).xrp() < newReserve) if (STAmount((*sleAccount)[sfBalance]).xrp() < newReserve)
return tecINSUFFICIENT_RESERVE; return tecINSUFFICIENT_RESERVE;
adjustOwnerCount(view, sleAccount, 1, j); adjustOwnerCount(view, sleAccount, hookStateScale, j);
} }
// update state count // update state count
@@ -1451,7 +1451,7 @@ lookup_state_cache(
if (stateMap.find(acc) == stateMap.end()) if (stateMap.find(acc) == stateMap.end())
return std::nullopt; return std::nullopt;
auto& stateMapAcc = std::get<2>(stateMap[acc]); auto& stateMapAcc = std::get<3>(stateMap[acc]);
if (stateMapAcc.find(ns) == stateMapAcc.end()) if (stateMapAcc.find(ns) == stateMapAcc.end())
return std::nullopt; return std::nullopt;
@@ -1486,6 +1486,7 @@ set_state_cache(
if (stateMap.find(acc) == stateMap.end()) if (stateMap.find(acc) == stateMap.end())
{ {
// new Account Key
// if this is the first time this account has been interacted with // if this is the first time this account has been interacted with
// we will compute how many available reserve positions there are // we will compute how many available reserve positions there are
auto const& fees = hookCtx.applyCtx.view().fees(); auto const& fees = hookCtx.applyCtx.view().fees();
@@ -1497,6 +1498,10 @@ set_state_cache(
STAmount bal = accSLE->getFieldAmount(sfBalance); STAmount bal = accSLE->getFieldAmount(sfBalance);
uint16_t const hookStateScale = accSLE->isFieldPresent(sfHookStateScale)
? accSLE->getFieldU16(sfHookStateScale)
: 1;
int64_t availableForReserves = bal.xrp().drops() - int64_t availableForReserves = bal.xrp().drops() -
fees.accountReserve(accSLE->getFieldU32(sfOwnerCount)).drops(); fees.accountReserve(accSLE->getFieldU32(sfOwnerCount)).drops();
@@ -1507,7 +1512,7 @@ set_state_cache(
availableForReserves /= increment; availableForReserves /= increment;
if (availableForReserves < 1 && modified) if (availableForReserves < hookStateScale && modified)
return RESERVE_INSUFFICIENT; return RESERVE_INSUFFICIENT;
int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces) int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces)
@@ -1526,20 +1531,28 @@ set_state_cache(
stateMap.modified_entry_count++; stateMap.modified_entry_count++;
// sanity check
if (view.rules().enabled(featureExtendedHookState) &&
availableForReserves < hookStateScale)
return INTERNAL_ERROR;
stateMap[acc] = { stateMap[acc] = {
availableForReserves - 1, availableForReserves - hookStateScale,
namespaceCount, namespaceCount,
hookStateScale,
{{ns, {{key, {modified, data}}}}}}; {{ns, {{key, {modified, data}}}}}};
return 1; return 1;
} }
auto& availableForReserves = std::get<0>(stateMap[acc]); auto& availableForReserves = std::get<0>(stateMap[acc]);
auto& namespaceCount = std::get<1>(stateMap[acc]); auto& namespaceCount = std::get<1>(stateMap[acc]);
auto& stateMapAcc = std::get<2>(stateMap[acc]); auto& hookStateScale = std::get<2>(stateMap[acc]);
bool const canReserveNew = availableForReserves > 0; auto& stateMapAcc = std::get<3>(stateMap[acc]);
bool const canReserveNew = availableForReserves >= hookStateScale;
if (stateMapAcc.find(ns) == stateMapAcc.end()) if (stateMapAcc.find(ns) == stateMapAcc.end())
{ {
// new Namespace Key
if (modified) if (modified)
{ {
if (!canReserveNew) if (!canReserveNew)
@@ -1557,7 +1570,11 @@ set_state_cache(
namespaceCount++; namespaceCount++;
} }
availableForReserves--; if (view.rules().enabled(featureExtendedHookState) &&
availableForReserves < hookStateScale)
return INTERNAL_ERROR;
availableForReserves -= hookStateScale;
stateMap.modified_entry_count++; stateMap.modified_entry_count++;
} }
@@ -1569,11 +1586,17 @@ set_state_cache(
auto& stateMapNs = stateMapAcc[ns]; auto& stateMapNs = stateMapAcc[ns];
if (stateMapNs.find(key) == stateMapNs.end()) if (stateMapNs.find(key) == stateMapNs.end())
{ {
// new State Key
if (modified) if (modified)
{ {
if (!canReserveNew) if (!canReserveNew)
return RESERVE_INSUFFICIENT; return RESERVE_INSUFFICIENT;
availableForReserves--;
if (view.rules().enabled(featureExtendedHookState) &&
availableForReserves < hookStateScale)
return INTERNAL_ERROR;
availableForReserves -= hookStateScale;
stateMap.modified_entry_count++; stateMap.modified_entry_count++;
} }
@@ -1582,6 +1605,7 @@ set_state_cache(
return 1; return 1;
} }
// existing State Key
if (modified) if (modified)
{ {
if (!stateMapNs[key].first) if (!stateMapNs[key].first)
@@ -1670,7 +1694,15 @@ DEFINE_HOOK_FUNCTION(
(aread_len && NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length))) (aread_len && NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length)))
return OUT_OF_BOUNDS; return OUT_OF_BOUNDS;
uint32_t maxSize = hook::maxHookStateDataSize(); auto const sleAccount = view.peek(hookCtx.result.accountKeylet);
if (!sleAccount && view.rules().enabled(featureExtendedHookState))
return tefINTERNAL;
uint16_t const hookStateScale = sleAccount->isFieldPresent(sfHookStateScale)
? sleAccount->getFieldU16(sfHookStateScale)
: 1;
uint32_t maxSize = hook::maxHookStateDataSize(hookStateScale);
if (read_len > maxSize) if (read_len > maxSize)
return TOO_BIG; return TOO_BIG;
@@ -1814,7 +1846,7 @@ hook::finalizeHookState(
for (const auto& accEntry : stateMap) for (const auto& accEntry : stateMap)
{ {
const auto& acc = accEntry.first; const auto& acc = accEntry.first;
for (const auto& nsEntry : std::get<2>(accEntry.second)) for (const auto& nsEntry : std::get<3>(accEntry.second))
{ {
const auto& ns = nsEntry.first; const auto& ns = nsEntry.first;
for (const auto& cacheEntry : nsEntry.second) for (const auto& cacheEntry : nsEntry.second)

View File

@@ -184,6 +184,19 @@ SetAccount::preflight(PreflightContext const& ctx)
return temMALFORMED; return temMALFORMED;
} }
// HookStateScale
if (tx.isFieldPresent(sfHookStateScale))
{
if (!ctx.rules.enabled(featureExtendedHookState))
return temMALFORMED;
uint16_t scale = tx.getFieldU16(sfHookStateScale);
if (scale == 0 ||
scale > hook::maxHookStateScale()) // Min: 1, Max: 16 (256
// * 16 = 4096 bytes)
return temMALFORMED;
}
return preflight2(ctx); return preflight2(ctx);
} }
@@ -249,6 +262,20 @@ SetAccount::preclaim(PreclaimContext const& ctx)
} }
} }
// HookStateScale
if (ctx.tx.isFieldPresent(sfHookStateScale))
{
uint16_t const newScale = ctx.tx.getFieldU16(sfHookStateScale);
uint16_t const currentScale = sle->getFieldU16(sfHookStateScale);
uint32_t const stateCount = sle->getFieldU32(sfHookStateCount);
if (stateCount > 0 && newScale < currentScale)
{
JLOG(ctx.j.trace())
<< "Cannot decrease HookStateScale if state count is not zero.";
return tecHAS_HOOK_STATE;
}
}
return tesSUCCESS; return tesSUCCESS;
} }
@@ -629,6 +656,50 @@ SetAccount::doApply()
if (uFlagsIn != uFlagsOut) if (uFlagsIn != uFlagsOut)
sle->setFieldU32(sfFlags, uFlagsOut); sle->setFieldU32(sfFlags, uFlagsOut);
// HookStateScale
if (tx.isFieldPresent(sfHookStateScale))
{
uint16_t const newScale = tx.getFieldU16(sfHookStateScale);
uint16_t const oldScale = sle->isFieldPresent(sfHookStateScale)
? sle->getFieldU16(sfHookStateScale)
: 1;
if (newScale == oldScale)
{
// do nothing
}
else if (newScale == 1)
{
sle->makeFieldAbsent(sfHookStateScale);
}
else
{
// increase OwnerCount
uint32_t const stateCount = sle->getFieldU32(sfHookStateCount);
uint32_t const oldOwnerCount = sle->getFieldU32(sfOwnerCount);
uint32_t const newOwnerCount = oldOwnerCount -
(oldScale * stateCount) + (newScale * stateCount);
// sanity check
if (newOwnerCount < oldOwnerCount)
return tecINTERNAL;
if (newOwnerCount != oldOwnerCount)
{
STAmount const balance = STAmount((*sle)[sfBalance]).xrp();
XRPAmount const reserve =
view().fees().accountReserve(newOwnerCount);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
adjustOwnerCount(
view(), sle, newOwnerCount - oldOwnerCount, j_);
}
sle->setFieldU16(sfHookStateScale, newScale);
}
}
return tesSUCCESS; return tesSUCCESS;
} }

View File

@@ -858,6 +858,9 @@ SetHook::destroyNamespace(
bool const fixEnabled = ctx.rules.enabled(fixNSDelete); bool const fixEnabled = ctx.rules.enabled(fixNSDelete);
bool partialDelete = false; bool partialDelete = false;
uint32_t oldStateCount = sleAccount->getFieldU32(sfHookStateCount); uint32_t oldStateCount = sleAccount->getFieldU32(sfHookStateCount);
uint16_t scale = sleAccount->isFieldPresent(sfHookStateScale)
? sleAccount->getFieldU16(sfHookStateScale)
: 1;
std::vector<uint256> toDelete; std::vector<uint256> toDelete;
toDelete.reserve(sleDir->getFieldV256(sfIndexes).size()); toDelete.reserve(sleDir->getFieldV256(sfIndexes).size());
@@ -929,6 +932,15 @@ SetHook::destroyNamespace(
view.erase(sleItem); view.erase(sleItem);
} }
if (view.rules().enabled(featureExtendedHookState) &&
oldStateCount < toDelete.size())
{
JLOG(ctx.j.fatal()) << "HookSet(" << hook::log::NSDELETE_COUNT << ")["
<< HS_ACC() << "]: DeleteState "
<< "stateCount less than zero (overflow)";
return tefBAD_LEDGER;
}
uint32_t stateCount = oldStateCount - toDelete.size(); uint32_t stateCount = oldStateCount - toDelete.size();
if (stateCount > oldStateCount) if (stateCount > oldStateCount)
{ {
@@ -945,7 +957,18 @@ SetHook::destroyNamespace(
sleAccount->setFieldU32(sfHookStateCount, stateCount); sleAccount->setFieldU32(sfHookStateCount, stateCount);
if (ctx.rules.enabled(fixNSDelete)) if (ctx.rules.enabled(fixNSDelete))
adjustOwnerCount(view, sleAccount, -toDelete.size(), ctx.j); {
auto const ownerCount = sleAccount->getFieldU32(sfOwnerCount);
if (view.rules().enabled(featureExtendedHookState) &&
ownerCount < toDelete.size() * scale)
{
JLOG(ctx.j.fatal()) << "HookSet(" << hook::log::NSDELETE_COUNT
<< ")[" << HS_ACC() << "]: DeleteState "
<< "OwnerCount less than zero (overflow)";
return tefBAD_LEDGER;
}
adjustOwnerCount(view, sleAccount, -toDelete.size() * scale, ctx.j);
}
if (!partialDelete && sleAccount->isFieldPresent(sfHookNamespaces)) if (!partialDelete && sleAccount->isFieldPresent(sfHookNamespaces))
hook::removeHookNamespaceEntry(*sleAccount, ns); hook::removeHookNamespaceEntry(*sleAccount, ns);

View File

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

View File

@@ -354,6 +354,7 @@ extern SF_UINT16 const sfHookStateChangeCount;
extern SF_UINT16 const sfHookEmitCount; extern SF_UINT16 const sfHookEmitCount;
extern SF_UINT16 const sfHookExecutionIndex; extern SF_UINT16 const sfHookExecutionIndex;
extern SF_UINT16 const sfHookApiVersion; extern SF_UINT16 const sfHookApiVersion;
extern SF_UINT16 const sfHookStateScale;
// 32-bit integers (common) // 32-bit integers (common)
extern SF_UINT32 const sfNetworkID; extern SF_UINT32 const sfNetworkID;

View File

@@ -343,6 +343,7 @@ enum TECcodes : TERUnderlyingType {
tecINSUF_RESERVE_SELLER = 187, tecINSUF_RESERVE_SELLER = 187,
tecIMMUTABLE = 188, tecIMMUTABLE = 188,
tecTOO_MANY_REMARKS = 189, tecTOO_MANY_REMARKS = 189,
tecHAS_HOOK_STATE = 190,
tecLAST_POSSIBLE_ENTRY = 255, tecLAST_POSSIBLE_ENTRY = 255,
}; };

View File

@@ -481,6 +481,7 @@ REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported // The following amendments are obsolete, but must remain supported
// because they could potentially get enabled. // because they could potentially get enabled.

View File

@@ -68,6 +68,7 @@ LedgerFormats::LedgerFormats()
{sfGovernanceMarks, soeOPTIONAL}, {sfGovernanceMarks, soeOPTIONAL},
{sfAccountIndex, soeOPTIONAL}, {sfAccountIndex, soeOPTIONAL},
{sfTouchCount, soeOPTIONAL}, {sfTouchCount, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL}, {sfCron, soeOPTIONAL},
}, },
commonFields); commonFields);

View File

@@ -102,6 +102,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookStateChangeCount, "HookStateChangeCount", UINT16,
CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16, 18); CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16, 18);
CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19); CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19);
CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20); CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20);
CONSTRUCT_TYPED_SFIELD(sfHookStateScale, "HookStateScale", UINT16, 21);
// 32-bit integers (common) // 32-bit integers (common)
CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1); CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1);

View File

@@ -94,6 +94,8 @@ transResults()
MAKE_ERROR(tecINSUF_RESERVE_SELLER, "The seller of an object has insufficient reserves, and thus cannot complete the sale."), MAKE_ERROR(tecINSUF_RESERVE_SELLER, "The seller of an object has insufficient reserves, and thus cannot complete the sale."),
MAKE_ERROR(tecIMMUTABLE, "The remark is marked immutable on the object, and therefore cannot be updated."), MAKE_ERROR(tecIMMUTABLE, "The remark is marked immutable on the object, and therefore cannot be updated."),
MAKE_ERROR(tecTOO_MANY_REMARKS, "The number of remarks on the object would exceed the limit of 32."), MAKE_ERROR(tecTOO_MANY_REMARKS, "The number of remarks on the object would exceed the limit of 32."),
MAKE_ERROR(tecHAS_HOOK_STATE, "Delete all hook state before reducing scale"),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."), MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."),

View File

@@ -60,6 +60,7 @@ TxFormats::TxFormats()
{sfTickSize, soeOPTIONAL}, {sfTickSize, soeOPTIONAL},
{sfTicketSequence, soeOPTIONAL}, {sfTicketSequence, soeOPTIONAL},
{sfNFTokenMinter, soeOPTIONAL}, {sfNFTokenMinter, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
}, },
commonFields); commonFields);

View File

@@ -879,6 +879,9 @@ public:
auto const bob = Account{"bob"}; auto const bob = Account{"bob"};
env.fund(XRP(10000), bob); env.fund(XRP(10000), bob);
auto const carol = Account{"carol"};
env.fund(XRP(10000), carol);
Json::Value jv; Json::Value jv;
jv[jss::Account] = alice.human(); jv[jss::Account] = alice.human();
jv[jss::TransactionType] = jss::SetHook; jv[jss::TransactionType] = jss::SetHook;
@@ -962,6 +965,7 @@ public:
data[3] == 'u' && data[4] == 'e' && data[5] == '\0'); data[3] == 'u' && data[4] == 'e' && data[5] == '\0');
BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2);
BEAST_EXPECT((*env.le(alice))[sfHookStateCount] == 1);
} }
// delete the namespace // delete the namespace
@@ -990,7 +994,113 @@ public:
// ensure the state object is gone // ensure the state object is gone
BEAST_EXPECT(!env.le(stateKeylet)); BEAST_EXPECT(!env.le(stateKeylet));
BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == fixNS ? 1 : 2); BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == (fixNS ? 1 : 2));
BEAST_EXPECT(!(env.le("alice")->isFieldPresent(sfHookStateCount)));
}
if (env.current()->rules().enabled(featureExtendedHookState))
{
// Test hook with scaled state data
TestHook scaled_state_wasm = wasm[
R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t
read_len, int64_t error_code); extern int64_t rollback
(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t state_set (uint32_t read_ptr, uint32_t
read_len, uint32_t kread_ptr, uint32_t kread_len);
extern int64_t util_keylet(uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t slot_set(uint32_t, uint32_t, uint32_t);
extern int64_t slot_subfield(uint32_t, uint32_t, uint32_t);
extern int64_t slot(uint32_t, uint32_t, uint32_t);
extern int64_t hook_account(uint32_t, uint32_t);
extern int64_t util_keylet(uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
#define SBUF(x) x, sizeof(x)
#define TOO_BIG -3
#define DOESNT_EXIST -5
#define KEYLET_ACCOUNT 3
#define sfHookStateScale ((1U << 16U) + 21U)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x,sizeof(#x),__LINE__)
int64_t hook(uint32_t reserved )
{
_g(1,1);
uint8_t hook_acc[20];
ASSERT(hook_account(hook_acc, 20) == 20);
uint8_t account_keylet[34];
ASSERT(util_keylet(account_keylet, 34, KEYLET_ACCOUNT,
hook_acc, 20, 0,0,0,0) == 34);
ASSERT(slot_set(account_keylet, 34, 1) == 1);
slot_subfield(1, sfHookStateScale, 2);
int64_t scale = slot(0,0,2);
if (scale == 5) {
ASSERT(state_set(0, 256, SBUF("test1")) == 256);
ASSERT(state_set(0, 256*2, SBUF("test2")) == 256*2);
ASSERT(state_set(0, 256*3, SBUF("test3")) == 256*3);
ASSERT(state_set(0, 256*4, SBUF("test4")) == 256*4);
ASSERT(state_set(0, 256*5, SBUF("test5")) == 256*5);
ASSERT(state_set(0, 256*5+1, SBUF("test")) == TOO_BIG);
accept(0,0,scale);
}
rollback(0,0,scale);
}
)[test.hook]"];
HASH_WASM(scaled_state);
BEAST_EXPECT(!env.le(carol)->isFieldPresent(sfHookStateCount));
// Install hook on carol
Json::Value jv =
ripple::test::jtx::hook(carol, {{hso(scaled_state_wasm)}}, 0);
jv[jss::Hooks][0U][jss::Hook][jss::HookNamespace] = ns_str;
jv[jss::Hooks][0U][jss::Hook][jss::HookOn] =
to_string(UINT256_BIT[ttACCOUNT_SET]);
env(jv, M("Create scaled state hook"), HSFEE, ter(tesSUCCESS));
env.close();
BEAST_EXPECT((*env.le(carol))[sfOwnerCount] == 1);
BEAST_EXPECT(!env.le(carol)->isFieldPresent(sfHookStateCount));
{
// HookStateScale => 5
Json::Value jv = noop(carol);
jv[sfHookStateScale.fieldName] = 5;
env(jv, HSFEE);
env.close();
BEAST_EXPECT((*env.le(carol))[sfOwnerCount] == 1);
BEAST_EXPECT(!env.le(carol)->isFieldPresent(sfHookStateCount));
Json::Value invoke = invoke::invoke(carol);
env(invoke, HSFEE);
env.close();
BEAST_EXPECT((*env.le(carol))[sfOwnerCount] == 26);
BEAST_EXPECT((*env.le(carol))[sfHookStateCount] == 5);
}
// Delete namespace to clean up state
Json::Value iv;
iv[jss::Flags] = hsfNSDELETE;
iv[jss::HookNamespace] = ns_str;
jv[jss::Hooks][0U][jss::Hook] = iv;
env(jv, M("Delete namespace"), HSFEE);
env.close();
// Verify state cleanup
BEAST_EXPECT(
(*env.le(carol))[sfOwnerCount] == features[fixNSDelete] ? 1
: 26);
BEAST_EXPECT(!env.le(carol)->isFieldPresent(sfHookStateCount));
} }
} }
@@ -9028,12 +9138,16 @@ public:
auto const david = Account{"david"}; auto const david = Account{"david"};
auto const eve = Account{"eve"}; // small balance auto const eve = Account{"eve"}; // small balance
auto const frank = Account{"frank"}; // big balance auto const frank = Account{"frank"}; // big balance
auto const gary = Account{"gary"};
auto const hank = Account{"hank"};
env.fund(XRP(10000), alice); env.fund(XRP(10000), alice);
env.fund(XRP(10000), bob); env.fund(XRP(10000), bob);
env.fund(XRP(10000), cho); env.fund(XRP(10000), cho);
env.fund(XRP(1000000), david); env.fund(XRP(1000000), david);
env.fund(XRP(2600), eve); env.fund(XRP(2600), eve);
env.fund(XRP(1000000000), frank); env.fund(XRP(1000000000), frank);
env.fund(XRP(10000), gary);
env.fund(XRP(10000), hank);
// install a rollback hook on cho // install a rollback hook on cho
env(ripple::test::jtx::hook( env(ripple::test::jtx::hook(
@@ -9058,6 +9172,101 @@ public:
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
} }
// bounds and buffer size checks
{
TestHook hook = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t state_set (
uint32_t read_ptr,
uint32_t read_len,
uint32_t kread_ptr,
uint32_t kread_len
);
extern int64_t otxn_param(uint32_t, uint32_t, uint32_t, uint32_t);
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define TOO_SMALL (-4)
#define TOO_BIG (-3)
#define OUT_OF_BOUNDS (-1)
int64_t hook(uint32_t reserved)
{
_g(1,1);
// bounds and buffer size checks
{
// RH NOTE: readptr/len 0/0 = delete entry
ASSERT(state_set(0,0,0,0) == TOO_SMALL);
ASSERT(state_set(0,0,0,33) == TOO_BIG);
ASSERT(state_set(0,0,0,1000000) == TOO_BIG);
ASSERT(state_set(0,0,1000000,1) == OUT_OF_BOUNDS);
ASSERT(state_set(0,1000000, 0, 32) == OUT_OF_BOUNDS);
ASSERT(state_set(1000000, 0, 0, 32) == OUT_OF_BOUNDS);
uint16_t size;
ASSERT(otxn_param(&size, 2, "SIZE", 4) > 0);
ASSERT(state_set(0, size, 0, 32) == TOO_BIG);
}
accept(0,0,0);
}
)[test.hook]"];
// install the hook on alice
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
M("set state_set 1"),
HSFEE);
env.close();
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
// invoke the hook with cho (rollback after alice's hooks have
// executed)
Json::Value payJv1 = pay(alice, cho, XRP(1));
{
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("SIZE"));
params[0U][jss::HookParameter][jss::HookParameterValue] =
features[featureExtendedHookState] ? "0108" /* 2049 */
: "0101" /* 257 */;
payJv1[jss::HookParameters] = params;
}
env(payJv1,
M("test state_set 1 rollback"),
fee(XRP(1)),
ter(tecHOOK_REJECTED));
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
auto const nsdir = env.le(nsdirkl);
BEAST_EXPECT(!nsdir);
auto const state1 = env.le(
ripple::keylet::hookState(aliceid, beast::zero, beast::zero));
BEAST_EXPECT(!state1);
// invoke the hook from bob to alice, this will work
Json::Value payJv2 = pay(bob, alice, XRP(1));
{
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("SIZE"));
params[0U][jss::HookParameter][jss::HookParameterValue] =
features[featureExtendedHookState] ? "0108" /* 2049 */
: "0101" /* 257 */;
payJv2[jss::HookParameters] = params;
}
env(payJv2, M("test state_set 1"), fee(XRP(1)));
env.close();
}
// first hook will set two state objects with different keys and data on // first hook will set two state objects with different keys and data on
// alice // alice
{ {
@@ -9094,32 +9303,12 @@ public:
#define ASSERT(x)\ #define ASSERT(x)\
if (!(x))\ if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__); rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define TOO_SMALL (-4)
#define TOO_BIG (-3)
#define OUT_OF_BOUNDS (-1)
#define SBUF(x) (uint32_t)(x), sizeof(x) #define SBUF(x) (uint32_t)(x), sizeof(x)
int64_t hook(uint32_t reserved) int64_t hook(uint32_t reserved)
{ {
_g(1,1); _g(1,1);
// bounds and buffer size checks
{
// RH NOTE: readptr/len 0/0 = delete entry
ASSERT(state_set(0,0,0,0) == TOO_SMALL);
ASSERT(state_set(0,0,0,33) == TOO_BIG);
ASSERT(state_set(0,0,0,1000000) == TOO_BIG);
ASSERT(state_set(0,0,1000000,1) == OUT_OF_BOUNDS);
ASSERT(state_set(0,1000000, 0, 32) == OUT_OF_BOUNDS);
ASSERT(state_set(1000000, 0, 0, 32) == OUT_OF_BOUNDS);
ASSERT(state_set(0, 257, 0, 32) == TOO_BIG);
}
// create state 1 // create state 1
{ {
uint8_t key[32] = uint8_t key[32] =
@@ -9135,7 +9324,6 @@ public:
0xCAU,0xFEU,0xBAU,0xBEU 0xCAU,0xFEU,0xBAU,0xBEU
}; };
ASSERT(state_set(SBUF(data), SBUF(key)) == sizeof(data)); ASSERT(state_set(SBUF(data), SBUF(key)) == sizeof(data));
} }
@@ -9146,11 +9334,9 @@ public:
1,2,3 1,2,3
}; };
ASSERT(state_set(SBUF(data2), SBUF(key)) == sizeof(data2)); ASSERT(state_set(SBUF(data2), SBUF(key)) == sizeof(data2));
} }
accept(0,0,0); accept(0,0,0);
} }
@@ -9719,6 +9905,210 @@ public:
BEAST_EXPECT((*env.le("frank"))[sfOwnerCount] == 260); BEAST_EXPECT((*env.le("frank"))[sfOwnerCount] == 260);
} }
if (env.current()->rules().enabled(featureExtendedHookState))
{
// Test hook with scaled state data
TestHook scaled_state_wasm = wasm[
R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t
read_len, int64_t error_code); extern int64_t rollback
(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t state_set (uint32_t read_ptr, uint32_t
read_len, uint32_t kread_ptr, uint32_t kread_len);
extern int64_t util_keylet(uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t slot_set(uint32_t, uint32_t, uint32_t);
extern int64_t slot_subfield(uint32_t, uint32_t, uint32_t);
extern int64_t slot(uint32_t, uint32_t, uint32_t);
extern int64_t hook_account(uint32_t, uint32_t);
extern int64_t util_keylet(uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
#define SBUF(x) x, sizeof(x)
#define TOO_BIG -3
#define DOESNT_EXIST -5
#define KEYLET_ACCOUNT 3
#define sfHookStateScale ((1U << 16U) + 21U)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x,sizeof(#x),__LINE__)
int64_t hook(uint32_t reserved )
{
_g(1,1);
uint8_t hook_acc[20];
ASSERT(hook_account(hook_acc, 20) == 20);
uint8_t account_keylet[34];
ASSERT(util_keylet(account_keylet, 34, KEYLET_ACCOUNT,
hook_acc, 20, 0,0,0,0) == 34);
ASSERT(slot_set(account_keylet, 34, 1) == 1);
slot_subfield(1, sfHookStateScale, 2);
int64_t scale = slot(0,0,2);
if (scale == DOESNT_EXIST) {
ASSERT(state_set(0, 256, SBUF("test0")) == 256);
ASSERT(state_set(0, 257, SBUF("test")) == TOO_BIG);
accept(0,0,scale);
}
if (scale == 2) {
ASSERT(state_set(0, 256, SBUF("test1")) == 256);
ASSERT(state_set(0, 256*2, SBUF("test2")) == 256*2);
ASSERT(state_set(0, 256*2+1, SBUF("test")) ==
TOO_BIG); accept(0,0,scale);
}
if (scale == 5) {
ASSERT(state_set(0, 256, SBUF("test3")) == 256);
ASSERT(state_set(0, 256*5, SBUF("test4")) == 256*5);
ASSERT(state_set(0, 256*5+1, SBUF("test")) ==
TOO_BIG); accept(0,0,scale);
}
rollback(0,0,scale);
}
)[test.hook]"];
HASH_WASM(scaled_state);
BEAST_EXPECT(!env.le(gary)->isFieldPresent(sfHookStateCount));
// Install hook on carol
Json::Value jv =
ripple::test::jtx::hook(gary, {{hso(scaled_state_wasm)}}, 0);
// jv[jss::Hooks][0U][jss::Hook][jss::HookNamespace] = ns_str;
jv[jss::Hooks][0U][jss::Hook][jss::HookOn] =
to_string(UINT256_BIT[ttACCOUNT_SET]);
env(jv, M("Create scaled state hook"), HSFEE, ter(tesSUCCESS));
env.close();
BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 1);
BEAST_EXPECT(!env.le(gary)->isFieldPresent(sfHookStateCount));
{
// no HookStateScale
Json::Value invoke = invoke::invoke(gary);
env(invoke, HSFEE);
env.close();
BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 2);
BEAST_EXPECT((*env.le(gary))[sfHookStateCount] == 1);
}
{
// HookStateScale => 2
Json::Value jv = noop(gary);
jv[sfHookStateScale.fieldName] = 2;
env(jv, HSFEE);
env.close();
BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 3);
BEAST_EXPECT((*env.le(gary))[sfHookStateCount] == 1);
Json::Value invoke = invoke::invoke(gary);
env(invoke, HSFEE);
env.close();
BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 7);
BEAST_EXPECT((*env.le(gary))[sfHookStateCount] == 3);
}
{
// HookStateScale => 5
Json::Value jv = noop(gary);
jv[sfHookStateScale.fieldName] = 5;
env(jv, HSFEE);
env.close();
BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 16);
BEAST_EXPECT((*env.le(gary))[sfHookStateCount] == 3);
Json::Value invoke = invoke::invoke(gary);
env(invoke, HSFEE);
env.close();
BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 26);
BEAST_EXPECT((*env.le(gary))[sfHookStateCount] == 5);
}
}
{
bool extHookStateEnabled = features[featureExtendedHookState];
// tests for set_state_cache
if (extHookStateEnabled)
{
TestHook extended_state_reserve_hook = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t state_set (
uint32_t read_ptr,
uint32_t read_len,
uint32_t kread_ptr,
uint32_t kread_len
);
extern int64_t state_foreign_set (
uint32_t read_ptr,
uint32_t read_len,
uint32_t kread_ptr,
uint32_t kread_len,
uint32_t nread_ptr,
uint32_t nread_len,
uint32_t aread_ptr,
uint32_t aread_len
);
extern int64_t otxn_param(uint32_t, uint32_t, uint32_t, uint32_t);
#define RESERVE_INSUFFICIENT -38
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define ASSERT_EQUAL(x, y)\
if (!(x == y))\
rollback((uint32_t)#x, sizeof(#x), x);
int64_t hook(uint32_t reserved)
{
_g(1,1);
{
// 1. first account for StateMap
ASSERT_EQUAL(state_set(0, 1, "1", 1), RESERVE_INSUFFICIENT);
// 2. first namespace for StateMap
ASSERT_EQUAL(state_foreign_set(0, 1, "1", 1, "1", 32, 0, 0), RESERVE_INSUFFICIENT);
// 3. first statekey for StateMap
ASSERT_EQUAL(state_set(0, 1, "2", 1), RESERVE_INSUFFICIENT);
// 4. existing statedata
ASSERT_EQUAL(state_set(0, 1, "1", 1), RESERVE_INSUFFICIENT);
}
accept(0,0,0);
}
)[test.hook]"];
// install the hook on gary
Json::Value jv = hso(extended_state_reserve_hook, overrideFlag);
jv[jss::HookOn] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // only invoke high
env(ripple::test::jtx::hook(hank, {{jv}}, 0), HSFEE);
env.close();
Json::Value jv1 = noop(hank);
jv1[sfHookStateScale.fieldName] = 8;
env(jv1, HSFEE);
env.close();
auto const caller = Account{"caller"};
env.fund(XRP(10000), caller);
env.close();
auto const payAmount = env.balance(hank) -
(env.current()->fees().accountReserve(1 + 8)) -
drops(1); // 8 + Hook
// reduce hank's balance
env(pay(hank, Account{"master"}, payAmount), fee(XRP(1)));
env.close();
// invoke the hook from alice
Json::Value invokeJv5 = invoke::invoke(caller, hank, "");
env(invokeJv5, M("test state_set 15"), fee(XRP(1)));
env.close();
}
}
// RH TODO: // RH TODO:
// check state can be set on emit callback // check state can be set on emit callback
// check namespacing provides for non-collision of same key // check namespacing provides for non-collision of same key
@@ -12884,14 +13274,17 @@ public:
using namespace test::jtx; using namespace test::jtx;
static FeatureBitset const all{supported_amendments()}; static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 6> const feats{ static std::array<FeatureBitset, 7> const feats{
all, all,
all - fixXahauV2, all - fixXahauV2,
all - fixXahauV1 - fixXahauV2, all - fixXahauV1 - fixXahauV2,
all - fixXahauV1 - fixXahauV2 - fixNSDelete, all - fixXahauV1 - fixXahauV2 - fixNSDelete,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap, all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap - all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
featureHookCanEmit}; featureHookCanEmit,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
featureExtendedHookState,
};
if (BEAST_EXPECT(instance < feats.size())) if (BEAST_EXPECT(instance < feats.size()))
{ {
@@ -13058,7 +13451,8 @@ SETHOOK_TEST(1, false)
SETHOOK_TEST(2, false) SETHOOK_TEST(2, false)
SETHOOK_TEST(3, false) SETHOOK_TEST(3, false)
SETHOOK_TEST(4, false) SETHOOK_TEST(4, false)
SETHOOK_TEST(5, true) SETHOOK_TEST(5, false)
SETHOOK_TEST(6, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
@@ -13066,6 +13460,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(SetHook2, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2);
} // namespace test } // namespace test
} // namespace ripple } // namespace ripple
#undef M #undef M

File diff suppressed because it is too large Load Diff

View File

@@ -544,6 +544,8 @@ public:
void void
testTicket() testTicket()
{ {
testcase("Ticket");
using namespace test::jtx; using namespace test::jtx;
Env env(*this); Env env(*this);
Account const alice("alice"); Account const alice("alice");
@@ -574,6 +576,104 @@ public:
env.close(); env.close();
} }
void
testHookStateScale()
{
testcase("HookStateScale");
using namespace test::jtx;
Env env(*this, supported_amendments() - featureExtendedHookState);
Account const alice("alice");
env.fund(XRP(10000), alice);
env.close();
// disabled
auto jt = noop(alice);
jt[sfHookStateScale.fieldName] = 1;
env(jt, ter(temMALFORMED));
env.close();
env.enableFeature(featureExtendedHookState);
env.close();
// set invalid HookStateScale (0 or > 16)
for (uint16_t scale : {0, 17})
{
jt[sfHookStateScale.fieldName] = scale;
env(jt, ter(temMALFORMED));
}
// set HookStateScale to 1
jt[sfHookStateScale.fieldName] = 1;
env(jt);
env.close();
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfHookStateScale));
// increase HookStateScale to 16
jt[sfHookStateScale.fieldName] = 16;
env(jt);
env.close();
BEAST_EXPECT(env.le(alice)->getFieldU16(sfHookStateScale) == 16);
// decrease HookStateScale to 8
jt[sfHookStateScale.fieldName] = 8;
env(jt);
env.close();
BEAST_EXPECT(env.le(alice)->getFieldU16(sfHookStateScale) == 8);
// reset HookStateScale
jt[sfHookStateScale.fieldName] = 1;
env(jt);
env.close();
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfHookStateScale));
// test OwnerCount
// This prevents an exception for sfMintedNFTokens when the AccountRoot
// template is applied.
{
uint256 const nftId0{token::getNextID(env, alice, 0u)};
env(token::mint(alice, 0u));
env(token::burn(alice, nftId0));
env.close();
}
auto applyCount = [&](uint16_t scale,
uint32_t stateCount,
uint32_t ownerCount) {
return env.app().openLedger().modify(
[&](OpenView& view, beast::Journal j) -> bool {
auto const sle = view.read(keylet::account(alice.id()));
if (!sle)
return false;
auto replacement = std::make_shared<SLE>(*sle, sle->key());
(*replacement)[sfHookStateScale] = scale;
(*replacement)[sfHookStateCount] = stateCount;
(*replacement)[sfOwnerCount] = ownerCount;
view.rawReplace(replacement);
return true;
});
};
applyCount(5, 10, 100);
// remove, but HookStateCount exists
jt[sfHookStateScale.fieldName] = 1;
env(jt, ter(tecHAS_HOOK_STATE));
// decrease, but HookStateCount exists
jt[sfHookStateScale.fieldName] = 4;
env(jt, ter(tecHAS_HOOK_STATE));
// increase
jt[sfHookStateScale.fieldName] = 6;
env(jt, ter(tesSUCCESS));
BEAST_EXPECT(env.le(alice)->getFieldU16(sfHookStateScale) == 6);
BEAST_EXPECT(env.le(alice)->getFieldU32(sfHookStateCount) == 10);
BEAST_EXPECT(env.le(alice)->getFieldU32(sfOwnerCount) == 110);
// check InsufficientReserve
applyCount(1, 100, 200);
jt[sfHookStateScale.fieldName] = 16;
env(jt, ter(tecINSUFFICIENT_RESERVE));
}
void void
run() override run() override
{ {
@@ -590,6 +690,7 @@ public:
testRequireAuthWithDir(); testRequireAuthWithDir();
testTransferRate(); testTransferRate();
testTicket(); testTicket();
testHookStateScale();
} }
}; };