mirror of
				https://github.com/Xahau/xahaud.git
				synced 2025-11-04 02:35:48 +00:00 
			
		
		
		
	ExtendedHookState (#406)
This commit is contained in:
		@@ -15,6 +15,7 @@
 | 
			
		||||
#define sfHookEmitCount ((1U << 16U) + 18U)
 | 
			
		||||
#define sfHookExecutionIndex ((1U << 16U) + 19U)
 | 
			
		||||
#define sfHookApiVersion ((1U << 16U) + 20U)
 | 
			
		||||
#define sfHookStateScale ((1U << 16U) + 21U)
 | 
			
		||||
#define sfNetworkID ((2U << 16U) + 1U)
 | 
			
		||||
#define sfFlags ((2U << 16U) + 2U)
 | 
			
		||||
#define sfSourceTag ((2U << 16U) + 3U)
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,8 @@ enum HookEmissionFlags : uint16_t {
 | 
			
		||||
};
 | 
			
		||||
}  // namespace ripple
 | 
			
		||||
 | 
			
		||||
using namespace ripple;
 | 
			
		||||
 | 
			
		||||
namespace hook {
 | 
			
		||||
// RH TODO: put these somewhere better, and allow rules to be fed in
 | 
			
		||||
inline uint32_t
 | 
			
		||||
@@ -43,10 +45,26 @@ maxHookParameterValueSize(void)
 | 
			
		||||
    return 256;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline uint32_t
 | 
			
		||||
maxHookStateDataSize(void)
 | 
			
		||||
inline uint16_t
 | 
			
		||||
maxHookStateScale(void)
 | 
			
		||||
{
 | 
			
		||||
    return 256U;
 | 
			
		||||
    return 16;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline uint32_t
 | 
			
		||||
maxHookStateDataSize(uint16_t hookStateScale)
 | 
			
		||||
{
 | 
			
		||||
    if (hookStateScale == 0)
 | 
			
		||||
    {
 | 
			
		||||
        // should not happen, but just in case
 | 
			
		||||
        return 256U;
 | 
			
		||||
    }
 | 
			
		||||
    if (hookStateScale > maxHookStateScale())
 | 
			
		||||
    {
 | 
			
		||||
        // should not happen, but just in case
 | 
			
		||||
        return 256 * maxHookStateScale();
 | 
			
		||||
    }
 | 
			
		||||
    return 256U * hookStateScale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline uint32_t
 | 
			
		||||
 
 | 
			
		||||
@@ -30,8 +30,9 @@ isEmittedTxn(ripple::STTx const& tx);
 | 
			
		||||
class HookStateMap : public std::map<
 | 
			
		||||
                         ripple::AccountID,  // account that owns the state
 | 
			
		||||
                         std::tuple<
 | 
			
		||||
                             int64_t,  // remaining available ownercount
 | 
			
		||||
                             int64_t,  // total namespace count
 | 
			
		||||
                             int64_t,   // remaining available ownercount
 | 
			
		||||
                             int64_t,   // total namespace count
 | 
			
		||||
                             uint16_t,  // hook state scale
 | 
			
		||||
                             std::map<
 | 
			
		||||
                                 ripple::uint256,  // namespace
 | 
			
		||||
                                 std::map<
 | 
			
		||||
@@ -465,9 +466,6 @@ apply(
 | 
			
		||||
 | 
			
		||||
struct HookContext;
 | 
			
		||||
 | 
			
		||||
uint32_t
 | 
			
		||||
computeHookStateOwnerCount(uint32_t hookStateCount);
 | 
			
		||||
 | 
			
		||||
int64_t
 | 
			
		||||
computeExecutionFee(uint64_t instructionCount);
 | 
			
		||||
int64_t
 | 
			
		||||
 
 | 
			
		||||
@@ -862,12 +862,6 @@ parseCurrency(uint8_t* cu_ptr, uint32_t cu_len)
 | 
			
		||||
        return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t
 | 
			
		||||
hook::computeHookStateOwnerCount(uint32_t hookStateCount)
 | 
			
		||||
{
 | 
			
		||||
    return hookStateCount;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline int64_t
 | 
			
		||||
serialize_keylet(
 | 
			
		||||
    ripple::Keylet& kl,
 | 
			
		||||
@@ -1080,14 +1074,18 @@ hook::setHookState(
 | 
			
		||||
        return tefINTERNAL;
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
 | 
			
		||||
    auto hookStateKeylet = ripple::keylet::hookState(acc, key, ns);
 | 
			
		||||
    auto hookStateDirKeylet = ripple::keylet::hookStateDir(acc, ns);
 | 
			
		||||
 | 
			
		||||
    uint32_t stateCount = sleAccount->getFieldU32(sfHookStateCount);
 | 
			
		||||
    uint32_t oldStateReserve = computeHookStateOwnerCount(stateCount);
 | 
			
		||||
    uint32_t oldStateCount = stateCount;
 | 
			
		||||
 | 
			
		||||
    auto hookState = view.peek(hookStateKeylet);
 | 
			
		||||
 | 
			
		||||
@@ -1118,13 +1116,15 @@ hook::setHookState(
 | 
			
		||||
        if (stateCount > 0)
 | 
			
		||||
            --stateCount;  // guard this because in the "impossible" event it is
 | 
			
		||||
                           // already 0 we'll wrap back to int_max
 | 
			
		||||
 | 
			
		||||
        // if removing this state entry would destroy the allotment then reduce
 | 
			
		||||
        // the owner count
 | 
			
		||||
        if (computeHookStateOwnerCount(stateCount) < oldStateReserve)
 | 
			
		||||
            adjustOwnerCount(view, sleAccount, -1, j);
 | 
			
		||||
        if (stateCount < oldStateCount)
 | 
			
		||||
            adjustOwnerCount(view, sleAccount, -hookStateScale, j);
 | 
			
		||||
 | 
			
		||||
        sleAccount->setFieldU32(sfHookStateCount, stateCount);
 | 
			
		||||
        if (view.rules().enabled(featureExtendedHookState) && stateCount == 0)
 | 
			
		||||
            sleAccount->makeFieldAbsent(sfHookStateCount);
 | 
			
		||||
        else
 | 
			
		||||
            sleAccount->setFieldU32(sfHookStateCount, stateCount);
 | 
			
		||||
 | 
			
		||||
        if (nsDestroyed)
 | 
			
		||||
            hook::removeHookNamespaceEntry(*sleAccount, ns);
 | 
			
		||||
@@ -1151,19 +1151,19 @@ hook::setHookState(
 | 
			
		||||
    {
 | 
			
		||||
        ++stateCount;
 | 
			
		||||
 | 
			
		||||
        if (computeHookStateOwnerCount(stateCount) > oldStateReserve)
 | 
			
		||||
        if (stateCount > oldStateCount)
 | 
			
		||||
        {
 | 
			
		||||
            // the hook used its allocated allotment of state entries for its
 | 
			
		||||
            // previous ownercount increment ownercount and give it another
 | 
			
		||||
            // allotment
 | 
			
		||||
 | 
			
		||||
            ++ownerCount;
 | 
			
		||||
            ownerCount += hookStateScale;
 | 
			
		||||
            XRPAmount const newReserve{view.fees().accountReserve(ownerCount)};
 | 
			
		||||
 | 
			
		||||
            if (STAmount((*sleAccount)[sfBalance]).xrp() < newReserve)
 | 
			
		||||
                return tecINSUFFICIENT_RESERVE;
 | 
			
		||||
 | 
			
		||||
            adjustOwnerCount(view, sleAccount, 1, j);
 | 
			
		||||
            adjustOwnerCount(view, sleAccount, hookStateScale, j);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // update state count
 | 
			
		||||
@@ -1451,7 +1451,7 @@ lookup_state_cache(
 | 
			
		||||
    if (stateMap.find(acc) == stateMap.end())
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
 | 
			
		||||
    auto& stateMapAcc = std::get<2>(stateMap[acc]);
 | 
			
		||||
    auto& stateMapAcc = std::get<3>(stateMap[acc]);
 | 
			
		||||
    if (stateMapAcc.find(ns) == stateMapAcc.end())
 | 
			
		||||
        return std::nullopt;
 | 
			
		||||
 | 
			
		||||
@@ -1486,6 +1486,7 @@ set_state_cache(
 | 
			
		||||
 | 
			
		||||
    if (stateMap.find(acc) == stateMap.end())
 | 
			
		||||
    {
 | 
			
		||||
        // new Account Key
 | 
			
		||||
        // if this is the first time this account has been interacted with
 | 
			
		||||
        // we will compute how many available reserve positions there are
 | 
			
		||||
        auto const& fees = hookCtx.applyCtx.view().fees();
 | 
			
		||||
@@ -1497,6 +1498,10 @@ set_state_cache(
 | 
			
		||||
 | 
			
		||||
        STAmount bal = accSLE->getFieldAmount(sfBalance);
 | 
			
		||||
 | 
			
		||||
        uint16_t const hookStateScale = accSLE->isFieldPresent(sfHookStateScale)
 | 
			
		||||
            ? accSLE->getFieldU16(sfHookStateScale)
 | 
			
		||||
            : 1;
 | 
			
		||||
 | 
			
		||||
        int64_t availableForReserves = bal.xrp().drops() -
 | 
			
		||||
            fees.accountReserve(accSLE->getFieldU32(sfOwnerCount)).drops();
 | 
			
		||||
 | 
			
		||||
@@ -1507,7 +1512,7 @@ set_state_cache(
 | 
			
		||||
 | 
			
		||||
        availableForReserves /= increment;
 | 
			
		||||
 | 
			
		||||
        if (availableForReserves < 1 && modified)
 | 
			
		||||
        if (availableForReserves < hookStateScale && modified)
 | 
			
		||||
            return RESERVE_INSUFFICIENT;
 | 
			
		||||
 | 
			
		||||
        int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces)
 | 
			
		||||
@@ -1526,20 +1531,28 @@ set_state_cache(
 | 
			
		||||
 | 
			
		||||
        stateMap.modified_entry_count++;
 | 
			
		||||
 | 
			
		||||
        // sanity check
 | 
			
		||||
        if (view.rules().enabled(featureExtendedHookState) &&
 | 
			
		||||
            availableForReserves < hookStateScale)
 | 
			
		||||
            return INTERNAL_ERROR;
 | 
			
		||||
 | 
			
		||||
        stateMap[acc] = {
 | 
			
		||||
            availableForReserves - 1,
 | 
			
		||||
            availableForReserves - hookStateScale,
 | 
			
		||||
            namespaceCount,
 | 
			
		||||
            hookStateScale,
 | 
			
		||||
            {{ns, {{key, {modified, data}}}}}};
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto& availableForReserves = std::get<0>(stateMap[acc]);
 | 
			
		||||
    auto& namespaceCount = std::get<1>(stateMap[acc]);
 | 
			
		||||
    auto& stateMapAcc = std::get<2>(stateMap[acc]);
 | 
			
		||||
    bool const canReserveNew = availableForReserves > 0;
 | 
			
		||||
    auto& hookStateScale = std::get<2>(stateMap[acc]);
 | 
			
		||||
    auto& stateMapAcc = std::get<3>(stateMap[acc]);
 | 
			
		||||
    bool const canReserveNew = availableForReserves >= hookStateScale;
 | 
			
		||||
 | 
			
		||||
    if (stateMapAcc.find(ns) == stateMapAcc.end())
 | 
			
		||||
    {
 | 
			
		||||
        // new Namespace Key
 | 
			
		||||
        if (modified)
 | 
			
		||||
        {
 | 
			
		||||
            if (!canReserveNew)
 | 
			
		||||
@@ -1557,7 +1570,11 @@ set_state_cache(
 | 
			
		||||
                namespaceCount++;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            availableForReserves--;
 | 
			
		||||
            if (view.rules().enabled(featureExtendedHookState) &&
 | 
			
		||||
                availableForReserves < hookStateScale)
 | 
			
		||||
                return INTERNAL_ERROR;
 | 
			
		||||
 | 
			
		||||
            availableForReserves -= hookStateScale;
 | 
			
		||||
            stateMap.modified_entry_count++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -1569,11 +1586,17 @@ set_state_cache(
 | 
			
		||||
    auto& stateMapNs = stateMapAcc[ns];
 | 
			
		||||
    if (stateMapNs.find(key) == stateMapNs.end())
 | 
			
		||||
    {
 | 
			
		||||
        // new State Key
 | 
			
		||||
        if (modified)
 | 
			
		||||
        {
 | 
			
		||||
            if (!canReserveNew)
 | 
			
		||||
                return RESERVE_INSUFFICIENT;
 | 
			
		||||
            availableForReserves--;
 | 
			
		||||
 | 
			
		||||
            if (view.rules().enabled(featureExtendedHookState) &&
 | 
			
		||||
                availableForReserves < hookStateScale)
 | 
			
		||||
                return INTERNAL_ERROR;
 | 
			
		||||
 | 
			
		||||
            availableForReserves -= hookStateScale;
 | 
			
		||||
            stateMap.modified_entry_count++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -1582,6 +1605,7 @@ set_state_cache(
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // existing State Key
 | 
			
		||||
    if (modified)
 | 
			
		||||
    {
 | 
			
		||||
        if (!stateMapNs[key].first)
 | 
			
		||||
@@ -1670,7 +1694,15 @@ DEFINE_HOOK_FUNCTION(
 | 
			
		||||
        (aread_len && NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length)))
 | 
			
		||||
        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)
 | 
			
		||||
        return TOO_BIG;
 | 
			
		||||
 | 
			
		||||
@@ -1814,7 +1846,7 @@ hook::finalizeHookState(
 | 
			
		||||
    for (const auto& accEntry : stateMap)
 | 
			
		||||
    {
 | 
			
		||||
        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;
 | 
			
		||||
            for (const auto& cacheEntry : nsEntry.second)
 | 
			
		||||
 
 | 
			
		||||
@@ -184,6 +184,19 @@ SetAccount::preflight(PreflightContext const& ctx)
 | 
			
		||||
            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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -629,6 +656,50 @@ SetAccount::doApply()
 | 
			
		||||
    if (uFlagsIn != 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -858,6 +858,9 @@ SetHook::destroyNamespace(
 | 
			
		||||
    bool const fixEnabled = ctx.rules.enabled(fixNSDelete);
 | 
			
		||||
    bool partialDelete = false;
 | 
			
		||||
    uint32_t oldStateCount = sleAccount->getFieldU32(sfHookStateCount);
 | 
			
		||||
    uint16_t scale = sleAccount->isFieldPresent(sfHookStateScale)
 | 
			
		||||
        ? sleAccount->getFieldU16(sfHookStateScale)
 | 
			
		||||
        : 1;
 | 
			
		||||
 | 
			
		||||
    std::vector<uint256> toDelete;
 | 
			
		||||
    toDelete.reserve(sleDir->getFieldV256(sfIndexes).size());
 | 
			
		||||
@@ -929,6 +932,15 @@ SetHook::destroyNamespace(
 | 
			
		||||
        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();
 | 
			
		||||
    if (stateCount > oldStateCount)
 | 
			
		||||
    {
 | 
			
		||||
@@ -945,7 +957,18 @@ SetHook::destroyNamespace(
 | 
			
		||||
        sleAccount->setFieldU32(sfHookStateCount, stateCount);
 | 
			
		||||
 | 
			
		||||
    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))
 | 
			
		||||
        hook::removeHookNamespaceEntry(*sleAccount, ns);
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@ namespace detail {
 | 
			
		||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
 | 
			
		||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
 | 
			
		||||
// the actual number of amendments. A LogicError on startup will verify this.
 | 
			
		||||
static constexpr std::size_t numFeatures = 87;
 | 
			
		||||
static constexpr std::size_t numFeatures = 88;
 | 
			
		||||
 | 
			
		||||
/** Amendments that this server supports and the default voting behavior.
 | 
			
		||||
   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 featureCron;
 | 
			
		||||
extern uint256 const fixInvalidTxFlags;
 | 
			
		||||
extern uint256 const featureExtendedHookState;
 | 
			
		||||
 | 
			
		||||
}  // namespace ripple
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -354,6 +354,7 @@ extern SF_UINT16 const sfHookStateChangeCount;
 | 
			
		||||
extern SF_UINT16 const sfHookEmitCount;
 | 
			
		||||
extern SF_UINT16 const sfHookExecutionIndex;
 | 
			
		||||
extern SF_UINT16 const sfHookApiVersion;
 | 
			
		||||
extern SF_UINT16 const sfHookStateScale;
 | 
			
		||||
 | 
			
		||||
// 32-bit integers (common)
 | 
			
		||||
extern SF_UINT32 const sfNetworkID;
 | 
			
		||||
 
 | 
			
		||||
@@ -343,6 +343,7 @@ enum TECcodes : TERUnderlyingType {
 | 
			
		||||
    tecINSUF_RESERVE_SELLER = 187,
 | 
			
		||||
    tecIMMUTABLE = 188,
 | 
			
		||||
    tecTOO_MANY_REMARKS = 189,
 | 
			
		||||
    tecHAS_HOOK_STATE = 190,
 | 
			
		||||
    tecLAST_POSSIBLE_ENTRY = 255,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -481,6 +481,7 @@ REGISTER_FEATURE(DeepFreeze,                    Supported::yes, VoteBehavior::De
 | 
			
		||||
REGISTER_FEATURE(IOUIssuerWeakTSH,              Supported::yes, VoteBehavior::DefaultNo);
 | 
			
		||||
REGISTER_FEATURE(Cron,                          Supported::yes, VoteBehavior::DefaultNo);
 | 
			
		||||
REGISTER_FIX    (fixInvalidTxFlags,             Supported::yes, VoteBehavior::DefaultYes);
 | 
			
		||||
REGISTER_FEATURE(ExtendedHookState,             Supported::yes, VoteBehavior::DefaultNo);
 | 
			
		||||
 | 
			
		||||
// The following amendments are obsolete, but must remain supported
 | 
			
		||||
// because they could potentially get enabled.
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,7 @@ LedgerFormats::LedgerFormats()
 | 
			
		||||
            {sfGovernanceMarks,      soeOPTIONAL},
 | 
			
		||||
            {sfAccountIndex,         soeOPTIONAL},
 | 
			
		||||
            {sfTouchCount,           soeOPTIONAL},
 | 
			
		||||
            {sfHookStateScale,       soeOPTIONAL},
 | 
			
		||||
            {sfCron,                 soeOPTIONAL},
 | 
			
		||||
        },
 | 
			
		||||
        commonFields);
 | 
			
		||||
 
 | 
			
		||||
@@ -102,6 +102,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookStateChangeCount,  "HookStateChangeCount", UINT16,
 | 
			
		||||
CONSTRUCT_TYPED_SFIELD(sfHookEmitCount,         "HookEmitCount",        UINT16,    18);
 | 
			
		||||
CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex,    "HookExecutionIndex",   UINT16,    19);
 | 
			
		||||
CONSTRUCT_TYPED_SFIELD(sfHookApiVersion,        "HookApiVersion",       UINT16,    20);
 | 
			
		||||
CONSTRUCT_TYPED_SFIELD(sfHookStateScale,        "HookStateScale",       UINT16,    21);
 | 
			
		||||
 | 
			
		||||
// 32-bit integers (common)
 | 
			
		||||
CONSTRUCT_TYPED_SFIELD(sfNetworkID,             "NetworkID",            UINT32,     1);
 | 
			
		||||
 
 | 
			
		||||
@@ -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(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(tecHAS_HOOK_STATE,                "Delete all hook state before reducing scale"),
 | 
			
		||||
 | 
			
		||||
        MAKE_ERROR(tefALREADY,                     "The exact transaction was already in this ledger."),
 | 
			
		||||
        MAKE_ERROR(tefBAD_ADD_AUTH,                "Not authorized to add account."),
 | 
			
		||||
        MAKE_ERROR(tefBAD_AUTH,                    "Transaction's public key is not authorized."),
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,7 @@ TxFormats::TxFormats()
 | 
			
		||||
            {sfTickSize, soeOPTIONAL},
 | 
			
		||||
            {sfTicketSequence, soeOPTIONAL},
 | 
			
		||||
            {sfNFTokenMinter, soeOPTIONAL},
 | 
			
		||||
            {sfHookStateScale, soeOPTIONAL},
 | 
			
		||||
        },
 | 
			
		||||
        commonFields);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -879,6 +879,9 @@ public:
 | 
			
		||||
        auto const bob = Account{"bob"};
 | 
			
		||||
        env.fund(XRP(10000), bob);
 | 
			
		||||
 | 
			
		||||
        auto const carol = Account{"carol"};
 | 
			
		||||
        env.fund(XRP(10000), carol);
 | 
			
		||||
 | 
			
		||||
        Json::Value jv;
 | 
			
		||||
        jv[jss::Account] = alice.human();
 | 
			
		||||
        jv[jss::TransactionType] = jss::SetHook;
 | 
			
		||||
@@ -962,6 +965,7 @@ public:
 | 
			
		||||
                data[3] == 'u' && data[4] == 'e' && data[5] == '\0');
 | 
			
		||||
 | 
			
		||||
            BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2);
 | 
			
		||||
            BEAST_EXPECT((*env.le(alice))[sfHookStateCount] == 1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete the namespace
 | 
			
		||||
@@ -990,7 +994,113 @@ public:
 | 
			
		||||
 | 
			
		||||
            // ensure the state object is gone
 | 
			
		||||
            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 eve = Account{"eve"};      // small 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), bob);
 | 
			
		||||
        env.fund(XRP(10000), cho);
 | 
			
		||||
        env.fund(XRP(1000000), david);
 | 
			
		||||
        env.fund(XRP(2600), eve);
 | 
			
		||||
        env.fund(XRP(1000000000), frank);
 | 
			
		||||
        env.fund(XRP(10000), gary);
 | 
			
		||||
        env.fund(XRP(10000), hank);
 | 
			
		||||
 | 
			
		||||
        // install a rollback hook on cho
 | 
			
		||||
        env(ripple::test::jtx::hook(
 | 
			
		||||
@@ -9058,6 +9172,101 @@ public:
 | 
			
		||||
            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
 | 
			
		||||
        // alice
 | 
			
		||||
        {
 | 
			
		||||
@@ -9094,32 +9303,12 @@ public:
 | 
			
		||||
            #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)
 | 
			
		||||
           
 | 
			
		||||
            #define SBUF(x) (uint32_t)(x), sizeof(x)
 | 
			
		||||
            int64_t hook(uint32_t reserved )
 | 
			
		||||
            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); 
 | 
			
		||||
     
 | 
			
		||||
                    ASSERT(state_set(0, 257, 0, 32) == TOO_BIG);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                // create state 1
 | 
			
		||||
                {
 | 
			
		||||
                    uint8_t key[32] =
 | 
			
		||||
@@ -9130,27 +9319,24 @@ public:
 | 
			
		||||
                        0,0,0,0,0,0,0,0
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    uint8_t data[4] = 
 | 
			
		||||
                    uint8_t data[4] =
 | 
			
		||||
                    {
 | 
			
		||||
                        0xCAU,0xFEU,0xBAU,0xBEU
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    ASSERT(state_set(SBUF(data), SBUF(key)) == sizeof(data));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // create state 2                
 | 
			
		||||
                // create state 2
 | 
			
		||||
                {
 | 
			
		||||
                    uint8_t key[3] =
 | 
			
		||||
                    {
 | 
			
		||||
                        1,2,3
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    ASSERT(state_set(SBUF(data2), SBUF(key)) == sizeof(data2));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                accept(0,0,0);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
@@ -9719,6 +9905,210 @@ public:
 | 
			
		||||
            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:
 | 
			
		||||
        // check state can be set on emit callback
 | 
			
		||||
        // check namespacing provides for non-collision of same key
 | 
			
		||||
@@ -12884,14 +13274,17 @@ public:
 | 
			
		||||
        using namespace test::jtx;
 | 
			
		||||
        static FeatureBitset const all{supported_amendments()};
 | 
			
		||||
 | 
			
		||||
        static std::array<FeatureBitset, 6> const feats{
 | 
			
		||||
        static std::array<FeatureBitset, 7> const feats{
 | 
			
		||||
            all,
 | 
			
		||||
            all - fixXahauV2,
 | 
			
		||||
            all - fixXahauV1 - fixXahauV2,
 | 
			
		||||
            all - fixXahauV1 - fixXahauV2 - fixNSDelete,
 | 
			
		||||
            all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap,
 | 
			
		||||
            all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
 | 
			
		||||
                featureHookCanEmit};
 | 
			
		||||
                featureHookCanEmit,
 | 
			
		||||
            all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
 | 
			
		||||
                featureExtendedHookState,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (BEAST_EXPECT(instance < feats.size()))
 | 
			
		||||
        {
 | 
			
		||||
@@ -13058,7 +13451,8 @@ SETHOOK_TEST(1, false)
 | 
			
		||||
SETHOOK_TEST(2, false)
 | 
			
		||||
SETHOOK_TEST(3, 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(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(SetHook4, app, ripple, 2);
 | 
			
		||||
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
 | 
			
		||||
BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2);
 | 
			
		||||
}  // namespace test
 | 
			
		||||
}  // namespace ripple
 | 
			
		||||
#undef M
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -544,6 +544,8 @@ public:
 | 
			
		||||
    void
 | 
			
		||||
    testTicket()
 | 
			
		||||
    {
 | 
			
		||||
        testcase("Ticket");
 | 
			
		||||
 | 
			
		||||
        using namespace test::jtx;
 | 
			
		||||
        Env env(*this);
 | 
			
		||||
        Account const alice("alice");
 | 
			
		||||
@@ -574,6 +576,104 @@ public:
 | 
			
		||||
        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
 | 
			
		||||
    run() override
 | 
			
		||||
    {
 | 
			
		||||
@@ -590,6 +690,7 @@ public:
 | 
			
		||||
        testRequireAuthWithDir();
 | 
			
		||||
        testTransferRate();
 | 
			
		||||
        testTicket();
 | 
			
		||||
        testHookStateScale();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user