Cron (on ledger cronjobs) (#590)

Co-authored-by: tequ <git@tequ.dev>
This commit is contained in:
RichardAH
2025-10-17 18:45:16 +10:00
committed by GitHub
parent 9c8b005406
commit 1ec31e79c9
28 changed files with 1470 additions and 12 deletions

View File

@@ -457,6 +457,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/CreateCheck.cpp
src/ripple/app/tx/impl/CreateOffer.cpp
src/ripple/app/tx/impl/CreateTicket.cpp
src/ripple/app/tx/impl/Cron.cpp
src/ripple/app/tx/impl/DeleteAccount.cpp
src/ripple/app/tx/impl/DepositPreauth.cpp
src/ripple/app/tx/impl/Escrow.cpp
@@ -474,6 +475,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/Payment.cpp
src/ripple/app/tx/impl/Remit.cpp
src/ripple/app/tx/impl/SetAccount.cpp
src/ripple/app/tx/impl/SetCron.cpp
src/ripple/app/tx/impl/SetHook.cpp
src/ripple/app/tx/impl/SetRemarks.cpp
src/ripple/app/tx/impl/SetRegularKey.cpp
@@ -734,6 +736,7 @@ if (tests)
src/test/app/BaseFee_test.cpp
src/test/app/Check_test.cpp
src/test/app/ClaimReward_test.cpp
src/test/app/Cron_test.cpp
src/test/app/Clawback_test.cpp
src/test/app/CrossingLimits_test.cpp
src/test/app/DeliverMin_test.cpp
@@ -898,6 +901,7 @@ if (tests)
src/test/jtx/impl/amount.cpp
src/test/jtx/impl/balance.cpp
src/test/jtx/impl/check.cpp
src/test/jtx/impl/cron.cpp
src/test/jtx/impl/delivermin.cpp
src/test/jtx/impl/deposit.cpp
src/test/jtx/impl/envconfig.cpp

View File

@@ -75,6 +75,11 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
switch (tt)
{
case ttCRON: {
ADD_TSH(tx.getAccountID(sfOwner), tshWEAK);
break;
}
case ttREMIT: {
if (destAcc)
ADD_TSH(*destAcc, tshSTRONG);

View File

@@ -1477,6 +1477,64 @@ TxQ::accept(Application& app, OpenView& view)
}
}
// Inject cron transactions, if any
if (view.rules().enabled(featureCron))
{
uint32_t currentTime =
view.parentCloseTime().time_since_epoch().count();
uint256 klStart = keylet::cron(0, AccountID(beast::zero)).key;
uint256 const klEnd =
keylet::cron(currentTime + 1, AccountID(beast::zero)).key;
std::set<AccountID> cronAccs;
auto counter = 0;
// include max 128 cron txns in the ledger
while (++counter < 129 && klStart < klEnd)
{
std::optional<uint256 const> next = view.succ(klStart, klEnd);
if (!next.has_value())
break;
Keylet kl{ltANY, *next};
if (view.exists(kl))
{
auto sle = view.read(kl);
if (safe_cast<LedgerEntryType>(
sle->getFieldU16(sfLedgerEntryType)) == ltCRON)
{
// valid cron object, add it to the list
cronAccs.emplace(sle->getAccountID(sfOwner));
}
}
klStart = *next;
}
auto const seq = view.info().seq;
// insert Cron pseudos for each of the accs we need to ping
for (AccountID const& id : cronAccs)
{
STTx cronTx(ttCRON, [=](auto& obj) {
obj[sfAccount] = AccountID();
obj[sfLedgerSequence] = seq;
obj[sfOwner] = id;
});
uint256 txID = cronTx.getTransactionID();
auto s = std::make_shared<ripple::Serializer>();
cronTx.add(*s);
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
app.getHashRouter().setFlags(txID, SF_EMITTED);
view.rawTxInsert(txID, std::move(s), nullptr);
ledgerChanged = true;
}
}
// Inject emitted transactions if any
if (view.rules().enabled(featureHooks))
do

View File

@@ -0,0 +1,188 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/tx/impl/Cron.h>
#include <ripple/basics/Log.h>
#include <ripple/core/Config.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/Quality.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/st.h>
namespace ripple {
TxConsequences
Cron::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Cron::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureCron))
return temDISABLED;
auto const ret = preflight0(ctx);
if (!isTesSuccess(ret))
return ret;
auto account = ctx.tx.getAccountID(sfAccount);
if (account != beast::zero)
{
JLOG(ctx.j.warn()) << "Cron: Bad source id";
return temBAD_SRC_ACCOUNT;
}
// No point in going any further if the transaction fee is malformed.
auto const fee = ctx.tx.getFieldAmount(sfFee);
if (!fee.native() || fee != beast::zero)
{
JLOG(ctx.j.warn()) << "Cron: invalid fee";
return temBAD_FEE;
}
if (!ctx.tx.getSigningPubKey().empty() || !ctx.tx.getSignature().empty() ||
ctx.tx.isFieldPresent(sfSigners))
{
JLOG(ctx.j.warn()) << "Cron: Bad signature";
return temBAD_SIGNATURE;
}
if (ctx.tx.getFieldU32(sfSequence) != 0 ||
ctx.tx.isFieldPresent(sfPreviousTxnID))
{
JLOG(ctx.j.warn()) << "Cron: Bad sequence";
return temBAD_SEQUENCE;
}
return tesSUCCESS;
}
TER
Cron::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureCron))
return temDISABLED;
return tesSUCCESS;
}
TER
Cron::doApply()
{
auto& view = ctx_.view();
auto const& tx = ctx_.tx;
AccountID const& id = tx.getAccountID(sfOwner);
auto sle = view.peek(keylet::account(id));
if (!sle)
{
// should never happen... but might in a race condition with acc delete
JLOG(j_.warn()) << "Cron: sfOwner account missing. " << id;
return tesSUCCESS;
}
if (!sle->isFieldPresent(sfCron))
{
JLOG(j_.warn()) << "Cron: sfCron missing from account " << id;
return tefINTERNAL;
}
uint256 ptr = sle->getFieldH256(sfCron);
Keylet klOld{ltCRON, ptr};
auto sleCron = view.peek(klOld);
if (!sleCron)
{
JLOG(j_.warn()) << "Cron: Cron object missing for account " << id;
return tesSUCCESS;
}
uint32_t delay = sleCron->getFieldU32(sfDelaySeconds);
uint32_t recur = sleCron->getFieldU32(sfRepeatCount);
uint32_t currentTime = view.parentCloseTime().time_since_epoch().count();
// do all this sanity checking before we modify the ledger...
uint32_t afterTime = currentTime + delay;
if (afterTime < currentTime)
return tefINTERNAL;
// in all circumstances the Cron object is deleted...
// if there are further crons to do then a new one is created at the next
// time point
if (!view.dirRemove(
keylet::ownerDir(id), (*sleCron)[sfOwnerNode], klOld, false))
return tefBAD_LEDGER;
view.erase(sleCron);
if (recur == 0)
{
// already at last execution, stop here
adjustOwnerCount(view, sle, -1, j_);
sle->makeFieldAbsent(sfCron);
view.update(sle);
return tesSUCCESS;
}
// more executions remain, so create a new object
Keylet klCron = keylet::cron(afterTime, id);
// insert into owner dir, we don't need to check reserve because we've just
// deleted an object
auto const page =
view.dirInsert(keylet::ownerDir(id), klCron, describeOwnerDir(id));
if (!page)
return tecDIR_FULL;
sleCron = std::make_shared<SLE>(klCron);
sleCron->setFieldU64(sfOwnerNode, *page);
sleCron->setFieldU32(sfDelaySeconds, delay);
sleCron->setFieldU32(sfRepeatCount, recur - 1);
sleCron->setAccountID(sfOwner, id);
sle->setFieldH256(sfCron, klCron.key);
view.insert(sleCron);
view.update(sle);
return tesSUCCESS;
}
void
Cron::preCompute()
{
assert(account_ == beast::zero);
}
XRPAmount
Cron::calculateBaseFee(ReadView const& view, STTx const& tx)
{
return XRPAmount{0};
}
} // namespace ripple

View File

@@ -0,0 +1,60 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_CRON_H_INCLUDED
#define RIPPLE_TX_CRON_H_INCLUDED
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>
#include <ripple/core/Config.h>
#include <ripple/protocol/Indexes.h>
namespace ripple {
class Cron : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
{
}
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const&);
TER
doApply() override;
void
preCompute() override;
};
} // namespace ripple
#endif

View File

@@ -491,6 +491,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltNFTOKEN_PAGE:
case ltNFTOKEN_OFFER:
case ltURI_TOKEN:
case ltCRON:
case ltIMPORT_VLSEQ:
case ltUNL_REPORT:
break;

View File

@@ -0,0 +1,255 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/tx/impl/SetCron.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/st.h>
namespace ripple {
TxConsequences
SetCron::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetCron::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureCron))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto& tx = ctx.tx;
auto& j = ctx.j;
if (tx.getFlags() & tfCronSetMask)
{
JLOG(j.warn()) << "SetCron: Invalid flags set.";
return temINVALID_FLAG;
}
// DelaySeconds (D), RepeatCount (R)
// DR - Set Cron with Delay and Repeat
// D- - Set Cron (once off) with Delay only (repat implicitly 0)
// -R - Invalid
// -- - Clear any existing cron (succeeds even if there isn't one) / with
// tfCronUnset flag set
bool const hasDelay = tx.isFieldPresent(sfDelaySeconds);
bool const hasRepeat = tx.isFieldPresent(sfRepeatCount);
if (tx.isFlag(tfCronUnset))
{
if (hasDelay || hasRepeat)
{
JLOG(j.debug()) << "SetCron: tfCronUnset flag cannot be used with "
"DelaySeconds or RepeatCount.";
return temMALFORMED;
}
}
else
{
if (!hasDelay)
{
JLOG(j.debug()) << "SetCron: DelaySeconds must be "
"specified to create a cron.";
return temMALFORMED;
}
// check delay is not too high
auto delay = tx.getFieldU32(sfDelaySeconds);
if (delay > 31536000UL /* 365 days in seconds */)
{
JLOG(j.debug()) << "SetCron: DelaySeconds was too high. (max 365 "
"days in seconds).";
return temMALFORMED;
}
// check repeat is not too high
if (hasRepeat)
{
auto recur = tx.getFieldU32(sfRepeatCount);
if (recur > 256)
{
JLOG(j.debug())
<< "SetCron: RepeatCount too high. Limit is 256. Issue "
"new SetCron to increase.";
return temMALFORMED;
}
}
}
return preflight2(ctx);
}
TER
SetCron::preclaim(PreclaimContext const& ctx)
{
return tesSUCCESS;
}
TER
SetCron::doApply()
{
auto& view = ctx_.view();
auto const& tx = ctx_.tx;
bool const isDelete = tx.isFlag(tfCronUnset);
// delay can be zero, in which case the cron will usually execute next
// ledger.
uint32_t delay{0};
uint32_t recur{0};
if (!isDelete)
{
if (tx.isFieldPresent(sfDelaySeconds))
delay = tx.getFieldU32(sfDelaySeconds);
if (tx.isFieldPresent(sfRepeatCount))
recur = tx.getFieldU32(sfRepeatCount);
}
uint32_t currentTime = view.parentCloseTime().time_since_epoch().count();
// do all this sanity checking before we modify the ledger...
// even for a delete operation this will fall through without incident
uint32_t afterTime = currentTime + delay;
if (afterTime < currentTime)
return tefINTERNAL;
AccountID const& id = tx.getAccountID(sfAccount);
auto sle = view.peek(keylet::account(id));
if (!sle)
return tefINTERNAL;
// in all cases whatsoever, this transaction will delete an existing
// old cron object and return the owner reserve to the owner.
if (sle->isFieldPresent(sfCron))
{
Keylet klOld{ltCRON, sle->getFieldH256(sfCron)};
auto sleCron = view.peek(klOld);
if (!sleCron)
{
JLOG(j_.warn()) << "SetCron: Cron object didn't exist.";
return tefBAD_LEDGER;
}
if (safe_cast<LedgerEntryType>(
sleCron->getFieldU16(sfLedgerEntryType)) != ltCRON)
{
JLOG(j_.warn()) << "SetCron: sfCron pointed to non-cron object!!";
return tefBAD_LEDGER;
}
if (!view.dirRemove(
keylet::ownerDir(id), (*sleCron)[sfOwnerNode], klOld, false))
{
JLOG(j_.warn()) << "SetCron: Ownerdir bad. " << id;
return tefBAD_LEDGER;
}
view.erase(sleCron);
adjustOwnerCount(view, sle, -1, j_);
sle->makeFieldAbsent(sfCron);
}
// if the operation is a delete (no delay or recur specified then stop
// here.)
if (isDelete)
{
view.update(sle);
return tesSUCCESS;
}
// execution to here means we're creating a new Cron object and adding it to
// the user's owner dir
Keylet klCron = keylet::cron(afterTime, id);
std::shared_ptr<SLE> sleCron = std::make_shared<SLE>(klCron);
STAmount const reserve{
view.fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1)};
STAmount const afterFee =
mPriorBalance - ctx_.tx.getFieldAmount(sfFee).xrp();
if (afterFee > mPriorBalance || afterFee < reserve)
return tecINSUFFICIENT_RESERVE;
// add to owner dir
auto const page =
view.dirInsert(keylet::ownerDir(id), klCron, describeOwnerDir(id));
if (!page)
return tecDIR_FULL;
sleCron->setFieldU64(sfOwnerNode, *page);
adjustOwnerCount(view, sle, 1, j_);
// set the fields
sleCron->setFieldU32(sfDelaySeconds, delay);
sleCron->setFieldU32(sfRepeatCount, recur);
sleCron->setAccountID(sfOwner, id);
sle->setFieldH256(sfCron, klCron.key);
view.update(sle);
view.insert(sleCron);
return tesSUCCESS;
}
XRPAmount
SetCron::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const baseFee = Transactor::calculateBaseFee(view, tx);
auto const hasRepeat = tx.isFieldPresent(sfRepeatCount);
if (tx.isFlag(tfCronUnset))
// delete cron
return baseFee;
// factor a cost based on the total number of txns expected
// for RepeatCount of 0 we have this txn (SetCron) and the
// single Cron txn (2). For a RepeatCount of 1 we have this txn,
// the first time the cron executes, and the second time (3).
uint32_t const additionalExpectedExecutions =
hasRepeat ? tx.getFieldU32(sfRepeatCount) + 1 : 1;
auto const additionalFee = baseFee * additionalExpectedExecutions;
if (baseFee + additionalFee < baseFee)
return baseFee;
return baseFee + additionalFee;
}
} // namespace ripple

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_SETCRON_H_INCLUDED
#define RIPPLE_TX_SETCRON_H_INCLUDED
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Indexes.h>
namespace ripple {
class SetCron : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit SetCron(ApplyContext& ctx) : Transactor(ctx)
{
}
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const&);
TER
doApply() override;
};
} // namespace ripple
#endif

View File

@@ -28,6 +28,7 @@
#include <ripple/app/tx/impl/CreateCheck.h>
#include <ripple/app/tx/impl/CreateOffer.h>
#include <ripple/app/tx/impl/CreateTicket.h>
#include <ripple/app/tx/impl/Cron.h>
#include <ripple/app/tx/impl/DeleteAccount.h>
#include <ripple/app/tx/impl/DepositPreauth.h>
#include <ripple/app/tx/impl/Escrow.h>
@@ -43,6 +44,7 @@
#include <ripple/app/tx/impl/Payment.h>
#include <ripple/app/tx/impl/Remit.h>
#include <ripple/app/tx/impl/SetAccount.h>
#include <ripple/app/tx/impl/SetCron.h>
#include <ripple/app/tx/impl/SetHook.h>
#include <ripple/app/tx/impl/SetRegularKey.h>
#include <ripple/app/tx/impl/SetRemarks.h>
@@ -181,6 +183,10 @@ invoke_preflight(PreflightContext const& ctx)
case ttURITOKEN_CREATE_SELL_OFFER:
case ttURITOKEN_CANCEL_SELL_OFFER:
return invoke_preflight_helper<URIToken>(ctx);
case ttCRON_SET:
return invoke_preflight_helper<SetCron>(ctx);
case ttCRON:
return invoke_preflight_helper<Cron>(ctx);
default:
assert(false);
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
@@ -306,6 +312,10 @@ invoke_preclaim(PreclaimContext const& ctx)
case ttURITOKEN_CREATE_SELL_OFFER:
case ttURITOKEN_CANCEL_SELL_OFFER:
return invoke_preclaim<URIToken>(ctx);
case ttCRON_SET:
return invoke_preclaim<SetCron>(ctx);
case ttCRON:
return invoke_preclaim<Cron>(ctx);
default:
assert(false);
return temUNKNOWN;
@@ -393,6 +403,10 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
case ttURITOKEN_CREATE_SELL_OFFER:
case ttURITOKEN_CANCEL_SELL_OFFER:
return URIToken::calculateBaseFee(view, tx);
case ttCRON_SET:
return SetCron::calculateBaseFee(view, tx);
case ttCRON:
return Cron::calculateBaseFee(view, tx);
default:
return XRPAmount{0};
}
@@ -586,6 +600,14 @@ invoke_apply(ApplyContext& ctx)
URIToken p(ctx);
return p();
}
case ttCRON_SET: {
SetCron p(ctx);
return p();
}
case ttCRON: {
Cron p(ctx);
return p();
}
default:
assert(false);
return {temUNKNOWN, false};

View File

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

View File

@@ -297,6 +297,9 @@ import_vlseq(PublicKey const& key) noexcept;
Keylet
uritoken(AccountID const& issuer, Blob const& uri);
Keylet
cron(uint32_t timestamp, AccountID const& id);
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:

View File

@@ -58,6 +58,12 @@ enum LedgerEntryType : std::uint16_t
*/
ltACCOUNT_ROOT = 0x0061,
/** A ledger object representing a scheduled cron execution on an account.
\sa keylet::cron
*/
ltCRON = 0x0041,
/** A ledger object which contains a list of object identifiers.
\sa keylet::page, keylet::quality, keylet::book, keylet::next and

View File

@@ -410,6 +410,8 @@ extern SF_UINT32 const sfRewardLgrLast;
extern SF_UINT32 const sfFirstNFTokenSequence;
extern SF_UINT32 const sfImportSequence;
extern SF_UINT32 const sfXahauActivationLgrSeq;
extern SF_UINT32 const sfDelaySeconds;
extern SF_UINT32 const sfRepeatCount;
// 64-bit integers (common)
extern SF_UINT64 const sfIndexNext;
@@ -486,6 +488,7 @@ extern SF_UINT256 const sfURITokenID;
extern SF_UINT256 const sfGovernanceFlags;
extern SF_UINT256 const sfGovernanceMarks;
extern SF_UINT256 const sfEmittedTxnID;
extern SF_UINT256 const sfCron;
// currency amount (common)
extern SF_AMOUNT const sfAmount;

View File

@@ -200,6 +200,12 @@ constexpr std::uint32_t const tfImmutable = 1;
// Clawback flags:
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
// CronSet Flags:
enum CronSetFlags : uint32_t {
tfCronUnset = 0x00000001,
};
constexpr std::uint32_t const tfCronSetMask = ~(tfUniversal | tfCronUnset);
// clang-format on
} // namespace ripple

View File

@@ -149,6 +149,12 @@ enum TxType : std::uint16_t
ttURITOKEN_CREATE_SELL_OFFER = 48,
ttURITOKEN_CANCEL_SELL_OFFER = 49,
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
ttCRON = 92,
/* Sechedule an alarm for later */
ttCRON_SET = 93,
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
ttREMARKS_SET = 94,

View File

@@ -479,6 +479,7 @@ REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::De
REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes);
// The following amendments are obsolete, but must remain supported

View File

@@ -72,6 +72,7 @@ enum class LedgerNameSpace : std::uint16_t {
URI_TOKEN = 'U',
IMPORT_VLSEQ = 'I',
UNL_REPORT = 'R',
CRON = 'A',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -443,6 +444,51 @@ uritoken(AccountID const& issuer, Blob const& uri)
LedgerNameSpace::URI_TOKEN, issuer, Slice{uri.data(), uri.size()})};
}
// Constructs an ordered CRON keylet (32 bytes):
// [8-byte namespace][4-byte timestamp (big-endian, seconds)][20-byte
// AccountID]
//
// Properties
// - Namespacing: first 8 bytes are the most-significant bytes of
// indexHash(LedgerNameSpace::CRON).
// - Uniqueness (per ts,acct): exactly ONE cron per (timestamp, AccountID).
// Insert is upsert (last-write-wins).
// This is fine because we only ever allow one cron per account at a time—if
// the same (ts,acct) is written, its simply an update to that single entry
// (idempotent; no duplicate leaves).
// - Iteration order: chronological by timestamp (BE), then by raw AccountID
// bytes.
// NOTE: raw AccountID ordering may bias priority; consider hashing AccountID
// for uniform per-timestamp spread.
// - Expected accidental prefix collisions (foreign objects sharing the 8-byte
// namespace): n / 2^64,
// assuming uniform high-64-bit distribution of other objects.
// Examples: 100M → ~5.4e-12, 1B → ~5.4e-11, 10B → ~5.4e-10, 100B → ~5.4e-9
// (negligible).
Keylet
cron(uint32_t timestamp, AccountID const& id)
{
static const uint256 ns = indexHash(LedgerNameSpace::CRON);
uint8_t h[32];
// first 8 bytes are the namespacing
std::memcpy(h, ns.data(), 8);
// next 4 bytes are the timestamp in BE
h[8] = static_cast<uint8_t>((timestamp >> 24) & 0xFFU);
h[9] = static_cast<uint8_t>((timestamp >> 16) & 0xFFU);
h[10] = static_cast<uint8_t>((timestamp >> 8) & 0xFFU);
h[11] = static_cast<uint8_t>((timestamp >> 0) & 0xFFU);
const uint256 accHash = indexHash(LedgerNameSpace::CRON, timestamp, id);
// final 20 bytes are account ID
std::memcpy(h + 12, accHash.cdata(), 20);
return {ltCRON, uint256::fromVoid(h)};
}
} // namespace keylet
} // namespace ripple

View File

@@ -68,6 +68,7 @@ LedgerFormats::LedgerFormats()
{sfGovernanceMarks, soeOPTIONAL},
{sfAccountIndex, soeOPTIONAL},
{sfTouchCount, soeOPTIONAL},
{sfCron, soeOPTIONAL},
},
commonFields);
@@ -366,6 +367,16 @@ LedgerFormats::LedgerFormats()
},
commonFields);
add(jss::Cron,
ltCRON,
{
{sfOwner, soeREQUIRED},
{sfDelaySeconds, soeREQUIRED},
{sfRepeatCount, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
},
commonFields);
// clang-format on
}

View File

@@ -157,6 +157,8 @@ CONSTRUCT_TYPED_SFIELD(sfLockCount, "LockCount", UINT32,
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
CONSTRUCT_TYPED_SFIELD(sfRepeatCount, "RepeatCount", UINT32, 94);
CONSTRUCT_TYPED_SFIELD(sfDelaySeconds, "DelaySeconds", UINT32, 95);
CONSTRUCT_TYPED_SFIELD(sfXahauActivationLgrSeq, "XahauActivationLgrSeq",UINT32, 96);
CONSTRUCT_TYPED_SFIELD(sfImportSequence, "ImportSequence", UINT32, 97);
CONSTRUCT_TYPED_SFIELD(sfRewardTime, "RewardTime", UINT32, 98);
@@ -239,6 +241,7 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceFlags, "GovernanceFlags", UINT256,
CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256, 98);
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95);
// currency amount (common)
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);

View File

@@ -615,7 +615,7 @@ isPseudoTx(STObject const& tx)
auto tt = safe_cast<TxType>(*t);
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT;
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
}
} // namespace ripple

View File

@@ -472,6 +472,22 @@ TxFormats::TxFormats()
{sfTicketSequence, soeOPTIONAL},
},
commonFields);
add(jss::Cron,
ttCRON,
{
{sfOwner, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
},
commonFields);
add(jss::CronSet,
ttCRON_SET,
{
{sfDelaySeconds, soeOPTIONAL},
{sfRepeatCount, soeOPTIONAL},
},
commonFields);
}
TxFormats const&

View File

@@ -51,14 +51,16 @@ JSS(Amendments); // ledger type.
JSS(Amount); // in: TransactionSign; field.
JSS(Authorize); // field
JSS(Blob);
JSS(Check); // ledger type.
JSS(CheckCancel); // transaction type.
JSS(CheckCash); // transaction type.
JSS(CheckCreate); // transaction type.
JSS(ClaimReward); // transaction type.
JSS(Clawback); // transaction type.
JSS(ClearFlag); // field.
JSS(CreateCode); // field.
JSS(Check); // ledger type.
JSS(CheckCancel); // transaction type.
JSS(CheckCash); // transaction type.
JSS(CheckCreate); // transaction type.
JSS(ClaimReward); // transaction type.
JSS(Clawback); // transaction type.
JSS(ClearFlag); // field.
JSS(CreateCode); // field.
JSS(Cron);
JSS(CronSet);
JSS(DeliverMin); // in: TransactionSign
JSS(DepositPreauth); // transaction and ledger type.
JSS(Destination); // in: TransactionSign; field.

View File

@@ -423,6 +423,7 @@ private:
addFlagsToJson<NFTokenMintFlags>(ret, "NFTokenMint");
addFlagsToJson<NFTokenCreateOfferFlags>(ret, "NFTokenCreateOffer");
addFlagsToJson<ClaimRewardFlags>(ret, "ClaimReward");
addFlagsToJson<CronSetFlags>(ret, "CronSet");
struct FlagData
{
std::string name;

461
src/test/app/Cron_test.cpp Normal file
View File

@@ -0,0 +1,461 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/app/ledger/LedgerMaster.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
namespace ripple {
namespace test {
struct Cron_test : public beast::unit_test::suite
{
void
testEnabled(FeatureBitset features)
{
testcase("enabled");
using namespace jtx;
using namespace std::literals::chrono_literals;
// setup env
auto const alice = Account("alice");
auto const issuer = Account("issuer");
for (bool const withCron : {false, true})
{
auto const amend = withCron ? features : features - featureCron;
Env env{*this, amend};
env.fund(XRP(1000), alice, issuer);
env.close();
auto const expectResult =
withCron ? ter(tesSUCCESS) : ter(temDISABLED);
auto tx = cron::set(alice);
// CLAIM
env(cron::set(alice), cron::delay(100), fee(XRP(1)), expectResult);
env.close();
}
}
void
testFee(FeatureBitset features)
{
testcase("fee");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
Env env{*this, features | featureCron};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(1000), alice);
env.close();
// create with RepeatCount
auto expected = baseFee * 2 + baseFee * 256;
env(cron::set(alice),
cron::delay(356 * 24 * 60 * 60),
cron::repeat(256),
fee(expected - 1),
ter(telINSUF_FEE_P));
env.close();
env(cron::set(alice),
cron::delay(356 * 24 * 60 * 60),
cron::repeat(256),
fee(expected),
ter(tesSUCCESS));
env.close();
// create with no RepeatCount
expected = baseFee * 2;
env(cron::set(alice),
cron::delay(356 * 24 * 60 * 60),
fee(expected - 1),
ter(telINSUF_FEE_P));
env.close();
env(cron::set(alice),
cron::delay(356 * 24 * 60 * 60),
fee(expected),
ter(tesSUCCESS));
env.close();
// delete
expected = baseFee;
env(cron::set(alice),
txflags(tfCronUnset),
fee(expected - 1),
ter(telINSUF_FEE_P));
env.close();
env(cron::set(alice),
txflags(tfCronUnset),
fee(expected),
ter(tesSUCCESS));
env.close();
}
void
testInvalidPreflight(FeatureBitset features)
{
testcase("invalid preflight");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
test::jtx::Env env{
*this, network::makeNetworkConfig(21337), features | featureCron};
env.fund(XRP(1000), alice);
env.close();
//----------------------------------------------------------------------
// preflight
// temINVALID_FLAG
{
env(cron::set(alice), txflags(tfClose), ter(temINVALID_FLAG));
env(cron::set(alice),
txflags(tfUniversalMask),
ter(temINVALID_FLAG));
}
// temMALFORMED
{
// Invalid both DelaySeconds and RepeatCount are not specified
env(cron::set(alice), ter(temMALFORMED));
// Invalid DelaySeconds and RepeatCount combination
// (only RepeatCount specified)
env(cron::set(alice), cron::repeat(256), ter(temMALFORMED));
// Invalid DelaySeconds
env(cron::set(alice),
cron::delay(365 * 24 * 60 * 60 + 1),
cron::repeat(256),
ter(temMALFORMED));
// Invalid RepeatCount
env(cron::set(alice),
cron::delay(365 * 24 * 60 * 60),
cron::repeat(257),
ter(temMALFORMED));
// Invalid tfCronUnset flag
env(cron::set(alice),
cron::delay(365 * 24 * 60 * 60),
txflags(tfCronUnset),
ter(temMALFORMED));
}
}
void
testInvalidPreclaim(FeatureBitset features)
{
testcase("invalid preclaim");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
Env env{*this, features | featureCron};
// there is no check in preclaim
BEAST_EXPECT(true);
}
void
testDoApply(FeatureBitset features)
{
testcase("doApply");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto const aliceOwnerCount = ownerCount(env, alice);
// create cron
env(cron::set(alice),
cron::delay(356 * 24 * 60 * 60),
cron::repeat(256),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// increment owner count
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount + 1);
auto const accSle = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle);
BEAST_EXPECT(accSle->isFieldPresent(sfCron));
auto const cronKey = keylet::child(accSle->getFieldH256(sfCron));
auto const cronSle = env.le(cronKey);
BEAST_EXPECT(cronSle);
BEAST_EXPECT(
cronSle->getFieldU32(sfDelaySeconds) == 356 * 24 * 60 * 60);
BEAST_EXPECT(cronSle->getFieldU32(sfRepeatCount) == 256);
// update cron
env(cron::set(alice),
cron::delay(100),
cron::repeat(10),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// owner count does not change
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount + 1);
auto const accSle2 = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle2);
BEAST_EXPECT(accSle2->isFieldPresent(sfCron));
// old cron sle is deleted
BEAST_EXPECT(!env.le(cronKey));
auto const cronKey2 = keylet::child(accSle2->getFieldH256(sfCron));
auto const cronSle2 = env.le(cronKey2);
BEAST_EXPECT(cronSle2);
BEAST_EXPECT(cronSle2->getFieldU32(sfDelaySeconds) == 100);
BEAST_EXPECT(cronSle2->getFieldU32(sfRepeatCount) == 10);
// delete cron
env(cron::set(alice),
fee(XRP(1)),
txflags(tfCronUnset),
ter(tesSUCCESS));
env.close();
// owner count decremented
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
auto const accSle3 = env.le(keylet::account(alice.id()));
BEAST_EXPECT(accSle3);
BEAST_EXPECT(!accSle3->isFieldPresent(sfCron));
// old cron sle is deleted
BEAST_EXPECT(!env.le(cronKey2));
// delete cron without object will succeed
env(cron::set(alice),
fee(XRP(1)),
txflags(tfCronUnset),
ter(tesSUCCESS));
env.close();
}
void
testCronExecution(FeatureBitset features)
{
testcase("cron execution");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
{
// test ttCron execution and repeatCount
Env env{*this, features | featureCron};
env.fund(XRP(1000), alice);
env.close();
auto baseTime = env.timeKeeper().now().time_since_epoch().count();
auto repeatCount = 10;
env(cron::set(alice),
cron::delay(100),
cron::repeat(repeatCount),
fee(XRP(1)));
env.close(10s);
auto lastCronKeylet =
keylet::child(env.le(alice)->getFieldH256(sfCron));
while (repeatCount >= 0)
{
// close ledger until 100 seconds has passed
while (env.timeKeeper().now().time_since_epoch().count() -
baseTime <
100)
{
env.close(10s);
auto txns = env.closed()->txs;
auto size = std::distance(txns.begin(), txns.end());
BEAST_EXPECT(size == 0);
}
// close after 100 seconds passed
env.close();
auto txns = env.closed()->txs;
auto size = std::distance(txns.begin(), txns.end());
BEAST_EXPECT(size == 1);
for (auto it = txns.begin(); it != txns.end(); ++it)
{
auto const& tx = *it->first;
// check pseudo txn format
BEAST_EXPECT(tx.getTxnType() == ttCRON);
BEAST_EXPECT(tx.getAccountID(sfAccount) == AccountID());
BEAST_EXPECT(tx.getAccountID(sfOwner) == alice.id());
BEAST_EXPECT(
tx.getFieldU32(sfLedgerSequence) ==
env.closed()->info().seq);
BEAST_EXPECT(tx.getFieldAmount(sfFee) == XRP(0));
BEAST_EXPECT(tx.getFieldVL(sfSigningPubKey).size() == 0);
// check old Cron object is deleted
BEAST_EXPECT(!env.le(lastCronKeylet));
if (repeatCount > 0)
{
// check new Cron object
auto const cronKeylet =
keylet::child(env.le(alice)->getFieldH256(sfCron));
auto const cronSle = env.le(cronKeylet);
BEAST_EXPECT(cronSle);
BEAST_EXPECT(
cronSle->getFieldU32(sfDelaySeconds) == 100);
BEAST_EXPECT(
cronSle->getFieldU32(sfRepeatCount) ==
--repeatCount);
BEAST_EXPECT(
cronSle->getAccountID(sfOwner) == alice.id());
// set new base time
baseTime =
env.timeKeeper().now().time_since_epoch().count();
lastCronKeylet = cronKeylet;
}
else
{
// after all executions, the cron object should be
// deleted
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfCron));
BEAST_EXPECT(!env.le(lastCronKeylet));
--repeatCount; // decrement for break double loop
}
}
}
}
{
// test ttCron limit in a ledger
Env env{*this, features | featureCron};
std::vector<Account> accounts;
accounts.reserve(300);
for (int i = 0; i < 300; ++i)
{
auto const& account = accounts.emplace_back(
Account("account_" + std::to_string(i)));
accounts.emplace_back(account);
env.fund(XRP(10000), account);
}
env.close();
for (auto const& account : accounts)
{
env(cron::set(account), cron::delay(0), fee(XRP(1)));
}
env.close();
// proceed ledger
env.close();
{
auto const txns = env.closed()->txs;
auto size = std::distance(txns.begin(), txns.end());
BEAST_EXPECT(size == 128);
for (auto it = txns.begin(); it != txns.end(); ++it)
{
auto const& tx = *it->first;
BEAST_EXPECT(tx.getTxnType() == ttCRON);
}
}
// proceed ledger
env.close();
{
auto const txns = env.closed()->txs;
auto size = std::distance(txns.begin(), txns.end());
BEAST_EXPECT(size == 128);
for (auto it = txns.begin(); it != txns.end(); ++it)
{
auto const& tx = *it->first;
BEAST_EXPECT(tx.getTxnType() == ttCRON);
}
}
// proceed ledger
env.close();
{
auto const txns = env.closed()->txs;
auto size = std::distance(txns.begin(), txns.end());
BEAST_EXPECT(size == 44);
for (auto it = txns.begin(); it != txns.end(); ++it)
{
auto const& tx = *it->first;
BEAST_EXPECT(tx.getTxnType() == ttCRON);
}
}
// proceed ledger
env.close();
{
auto const txns = env.closed()->txs;
auto size = std::distance(txns.begin(), txns.end());
BEAST_EXPECT(size == 0);
}
}
}
void
testWithFeats(FeatureBitset features)
{
testEnabled(features);
testFee(features);
testInvalidPreflight(features);
testInvalidPreclaim(features);
testDoApply(features);
testCronExecution(features);
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
}
};
BEAST_DEFINE_TESTSUITE(Cron, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -22,6 +22,7 @@
#include <ripple/app/misc/HashRouter.h>
#include <ripple/app/misc/TxQ.h>
#include <ripple/app/tx/apply.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/PayChan.h>
#include <ripple/protocol/jss.h>
@@ -748,10 +749,22 @@ private:
jtx::Env& env,
int const& expected,
uint64_t const& lineno)
{
auto const hashStr =
env.tx()->getJson(JsonOptions::none)[jss::hash].asString();
uint256 const txHash = uint256::fromVoid(strUnHex(hashStr)->data());
testTSHStrongWeak(env, txHash, expected, lineno);
}
void
testTSHStrongWeak(
jtx::Env& env,
uint256 const& txHash,
int const& expected,
uint64_t const& lineno)
{
Json::Value params;
params[jss::transaction] =
env.tx()->getJson(JsonOptions::none)[jss::hash];
params[jss::transaction] = strHex(txHash);
auto const jrr = env.rpc("json", "tx", to_string(params));
auto const meta = jrr[jss::result][jss::meta];
validateTSHStrongWeak(meta, expected, lineno);
@@ -6251,6 +6264,103 @@ private:
}
}
// CronSet
// | otxn | tsh | cset |
// | A | A | S |
void
testCronSetTSH(FeatureBitset features)
{
testcase("cron set tsh");
using namespace test::jtx;
using namespace std::literals;
// otxn: account
// tsh account
// w/s: strong
for (bool const testStrong : {true, false})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
auto const account = Account("alice");
env.fund(XRP(1000), account);
env.close();
if (!testStrong)
addWeakTSH(env, account);
// set tsh hook
setTSHHook(env, account, testStrong);
// cron set
env(cron::set(account),
cron::delay(100),
cron::repeat(1),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
testTSHStrongWeak(env, tshSTRONG, __LINE__);
}
}
// | otxn | tsh | cron |
// | - | O | W |
void
testCronTSH(FeatureBitset features)
{
testcase("cron tsh");
using namespace test::jtx;
using namespace std::literals;
// otxn: -
// tsh owner
// w/s: weak
for (bool const testStrong : {true, false})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
auto const account = Account("alice");
env.fund(XRP(1000), account);
env.close();
// cron set
env(cron::set(account),
cron::delay(100),
cron::repeat(1),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
if (!testStrong)
addWeakTSH(env, account);
// set tsh hook
setTSHHook(env, account, testStrong);
// proceed ledger
env.close(100s);
// close ledger
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshNONE : tshWEAK;
auto const txs = env.closed()->txs;
BEAST_EXPECT(std::distance(txs.begin(), txs.end()) == 1);
auto const tx = txs.begin()->first;
BEAST_EXPECT(tx->getTxnType() == ttCRON);
testTSHStrongWeak(env, tx->getTransactionID(), expected, __LINE__);
}
}
void
testEmissionOrdering(FeatureBitset features)
{
@@ -6398,6 +6508,8 @@ private:
testURITokenCancelSellOfferTSH(features);
testURITokenCreateSellOfferTSH(features);
testRemitTSH(features);
testCronSetTSH(features);
testCronTSH(features);
}
void

View File

@@ -33,6 +33,7 @@
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/check.h>
#include <test/jtx/cron.h>
#include <test/jtx/delivermin.h>
#include <test/jtx/deposit.h>
#include <test/jtx/escrow.h>

74
src/test/jtx/cron.h Normal file
View File

@@ -0,0 +1,74 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_CRON_H_INCLUDED
#define RIPPLE_TEST_JTX_CRON_H_INCLUDED
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
namespace ripple {
namespace test {
namespace jtx {
/** Cron operations. */
namespace cron {
/** Set a cron. */
Json::Value
set(jtx::Account const& account);
/** Sets the optional DelaySeconds on a JTx. */
class delay
{
private:
uint32_t delay_;
public:
explicit delay(uint32_t delay) : delay_(delay)
{
}
void
operator()(Env&, JTx& jtx) const;
};
/** Sets the optional RepeatCount on a JTx. */
class repeat
{
private:
uint32_t repeat_;
public:
explicit repeat(uint32_t repeat) : repeat_(repeat)
{
}
void
operator()(Env&, JTx& jtx) const;
};
} // namespace cron
} // namespace jtx
} // namespace test
} // namespace ripple
#endif // RIPPLE_TEST_JTX_CRON_H_INCLUDED

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/protocol/jss.h>
#include <test/jtx/cron.h>
namespace ripple {
namespace test {
namespace jtx {
namespace cron {
// Set a cron.
Json::Value
set(jtx::Account const& account)
{
using namespace jtx;
Json::Value jv;
jv[jss::TransactionType] = jss::CronSet;
jv[jss::Account] = account.human();
return jv;
}
void
delay::operator()(Env& env, JTx& jt) const
{
jt.jv[sfDelaySeconds.jsonName] = delay_;
}
void
repeat::operator()(Env& env, JTx& jt) const
{
jt.jv[sfRepeatCount.jsonName] = repeat_;
}
} // namespace cron
} // namespace jtx
} // namespace test
} // namespace ripple