mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-19 01:55:48 +00:00
Compare commits
6 Commits
cronjob
...
contrib-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4efcfaa43e | ||
|
|
6fa6a96e3a | ||
|
|
cbd7cd98ff | ||
|
|
913a4da31b | ||
|
|
b0fcd36bcd | ||
|
|
1ec31e79c9 |
@@ -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
|
||||
|
||||
@@ -176,10 +176,11 @@ existing maintainer without a vote.
|
||||
|
||||
## Current Maintainers
|
||||
|
||||
* [Richard Holland](https://github.com/RichardAH) (XRPL Labs + XRP Ledger Foundation)
|
||||
* [Denis Angell](https://github.com/dangell7) (XRPL Labs + XRP Ledger Foundation)
|
||||
* [Wietse Wind](https://github.com/WietseWind) (XRPL Labs + XRP Ledger Foundation)
|
||||
* [Richard Holland](https://github.com/RichardAH) (XRPL Labs + INFTF)
|
||||
* [Denis Angell](https://github.com/dangell7) (XRPL Labs + INFTF)
|
||||
* [Wietse Wind](https://github.com/WietseWind) (XRPL Labs + INFTF)
|
||||
* [tequ](https://github.com/tequdev) (Independent + INFTF)
|
||||
|
||||
|
||||
[1]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
|
||||
[2]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
|
||||
[2]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits
|
||||
|
||||
@@ -62,6 +62,9 @@
|
||||
#define sfEmitGeneration ((2U << 16U) + 46U)
|
||||
#define sfLockCount ((2U << 16U) + 49U)
|
||||
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
|
||||
#define sfStartTime ((2U << 16U) + 93U)
|
||||
#define sfRepeatCount ((2U << 16U) + 94U)
|
||||
#define sfDelaySeconds ((2U << 16U) + 95U)
|
||||
#define sfXahauActivationLgrSeq ((2U << 16U) + 96U)
|
||||
#define sfImportSequence ((2U << 16U) + 97U)
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
@@ -129,6 +132,7 @@
|
||||
#define sfGovernanceFlags ((5U << 16U) + 99U)
|
||||
#define sfGovernanceMarks ((5U << 16U) + 98U)
|
||||
#define sfEmittedTxnID ((5U << 16U) + 97U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
#define sfAmount ((6U << 16U) + 1U)
|
||||
#define sfBalance ((6U << 16U) + 2U)
|
||||
#define sfLimitAmount ((6U << 16U) + 3U)
|
||||
@@ -236,4 +240,4 @@
|
||||
#define sfActiveValidators ((15U << 16U) + 95U)
|
||||
#define sfImportVLKeys ((15U << 16U) + 94U)
|
||||
#define sfHookEmissions ((15U << 16U) + 93U)
|
||||
#define sfAmounts ((15U << 16U) + 92U)
|
||||
#define sfAmounts ((15U << 16U) + 92U)
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#define ttURITOKEN_BUY 47
|
||||
#define ttURITOKEN_CREATE_SELL_OFFER 48
|
||||
#define ttURITOKEN_CANCEL_SELL_OFFER 49
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
#define ttREMIT 95
|
||||
#define ttGENESIS_MINT 96
|
||||
#define ttIMPORT 97
|
||||
@@ -40,4 +42,4 @@
|
||||
#define ttFEE 101
|
||||
#define ttUNL_MODIFY 102
|
||||
#define ttEMIT_FAILURE 103
|
||||
#define ttUNL_REPORT 104
|
||||
#define ttUNL_REPORT 104
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,21 +94,6 @@ Change::preflight(PreflightContext const& ctx)
|
||||
"of sfImportVLKey, sfActiveValidator";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// if we do specify import_vl_keys in config then we won't approve keys
|
||||
// that aren't on our list
|
||||
if (ctx.tx.isFieldPresent(sfImportVLKey) &&
|
||||
!ctx.app.config().IMPORT_VL_KEYS.empty())
|
||||
{
|
||||
auto const& inner = const_cast<ripple::STTx&>(ctx.tx)
|
||||
.getField(sfImportVLKey)
|
||||
.downcast<STObject>();
|
||||
auto const pk = inner.getFieldVL(sfPublicKey);
|
||||
std::string const strPk = strHex(makeSlice(pk));
|
||||
if (ctx.app.config().IMPORT_VL_KEYS.find(strPk) ==
|
||||
ctx.app.config().IMPORT_VL_KEYS.end())
|
||||
return telIMPORT_VL_KEY_NOT_RECOGNISED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -168,9 +153,42 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
return tesSUCCESS;
|
||||
case ttUNL_REPORT: {
|
||||
if (!ctx.tx.isFieldPresent(sfImportVLKey) ||
|
||||
ctx.app.config().IMPORT_VL_KEYS.empty())
|
||||
return tesSUCCESS;
|
||||
|
||||
// if we do specify import_vl_keys in config then we won't approve
|
||||
// keys that aren't on our list and/or aren't in the ledger object
|
||||
auto const& inner = const_cast<ripple::STTx&>(ctx.tx)
|
||||
.getField(sfImportVLKey)
|
||||
.downcast<STObject>();
|
||||
auto const pkBlob = inner.getFieldVL(sfPublicKey);
|
||||
std::string const strPk = strHex(makeSlice(pkBlob));
|
||||
if (ctx.app.config().IMPORT_VL_KEYS.find(strPk) !=
|
||||
ctx.app.config().IMPORT_VL_KEYS.end())
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const pkType = publicKeyType(makeSlice(pkBlob));
|
||||
if (!pkType)
|
||||
return tefINTERNAL;
|
||||
|
||||
PublicKey const pk(makeSlice(pkBlob));
|
||||
|
||||
// check on ledger
|
||||
if (auto const unlRep = ctx.view.read(keylet::UNLReport());
|
||||
unlRep && unlRep->isFieldPresent(sfImportVLKeys))
|
||||
{
|
||||
auto const& vlKeys = unlRep->getFieldArray(sfImportVLKeys);
|
||||
for (auto const& k : vlKeys)
|
||||
if (PublicKey(k[sfPublicKey]) == pk)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
return telIMPORT_VL_KEY_NOT_RECOGNISED;
|
||||
}
|
||||
default:
|
||||
return temUNKNOWN;
|
||||
}
|
||||
|
||||
189
src/ripple/app/tx/impl/Cron.cpp
Normal file
189
src/ripple/app/tx/impl/Cron.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 lastStartTime = sleCron->getFieldU32(sfStartTime);
|
||||
|
||||
// do all this sanity checking before we modify the ledger...
|
||||
uint32_t afterTime = lastStartTime + delay;
|
||||
if (afterTime < lastStartTime)
|
||||
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->setFieldU32(sfStartTime, afterTime);
|
||||
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
|
||||
60
src/ripple/app/tx/impl/Cron.h
Normal file
60
src/ripple/app/tx/impl/Cron.h
Normal 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
|
||||
@@ -491,6 +491,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltNFTOKEN_PAGE:
|
||||
case ltNFTOKEN_OFFER:
|
||||
case ltURI_TOKEN:
|
||||
case ltCRON:
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
break;
|
||||
|
||||
305
src/ripple/app/tx/impl/SetCron.cpp
Normal file
305
src/ripple/app/tx/impl/SetCron.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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), StartTime (S)
|
||||
// DRS - Set Cron with Delay and Repeat and StartTime
|
||||
// DR- - Invalid(StartTime is required)
|
||||
// D-S - Invalid (both DelaySeconds and RepeatCount are required)
|
||||
// -RS - Invalid (both DelaySeconds and RepeatCount are required)
|
||||
// --S - Onetime cron with StartTime only
|
||||
// -- - 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);
|
||||
bool const hasStartTime = tx.isFieldPresent(sfStartTime);
|
||||
|
||||
if (tx.isFlag(tfCronUnset))
|
||||
{
|
||||
// delete operation
|
||||
if (hasDelay || hasRepeat || hasStartTime)
|
||||
{
|
||||
JLOG(j.debug()) << "SetCron: tfCronUnset flag cannot be used with "
|
||||
"DelaySeconds, RepeatCount or StartTime.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// create operation
|
||||
|
||||
if (!hasStartTime)
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "SetCron: StartTime is required. Use StartTime=0 for "
|
||||
"immediate execution, or specify a future timestamp.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if ((!hasDelay && hasRepeat) || (hasDelay && !hasRepeat))
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "SetCron: DelaySeconds and RepeatCount must both be present "
|
||||
"for recurring crons, or both absent for one-off crons.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// check delay is not too high
|
||||
if (hasDelay)
|
||||
{
|
||||
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 == 0)
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "SetCron: RepeatCount must be greater than 0."
|
||||
"For one-time execution, omit DelaySeconds and "
|
||||
"RepeatCount.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfStartTime) &&
|
||||
ctx.tx.getFieldU32(sfStartTime) != 0)
|
||||
{
|
||||
// StartTime 0 means the cron will execute immediately
|
||||
|
||||
auto const startTime = ctx.tx.getFieldU32(sfStartTime);
|
||||
auto const parentCloseTime =
|
||||
ctx.view.parentCloseTime().time_since_epoch().count();
|
||||
|
||||
if (startTime < parentCloseTime)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetCron: StartTime must be in the future "
|
||||
"(or 0 for immediate execution)";
|
||||
return tecEXPIRED;
|
||||
}
|
||||
|
||||
if (startTime > ctx.view.parentCloseTime().time_since_epoch().count() +
|
||||
365 * 24 * 60 * 60)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetCron: StartTime is too far in the "
|
||||
"future (max 365 days).";
|
||||
return tecEXPIRED;
|
||||
}
|
||||
}
|
||||
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};
|
||||
uint32_t startTime{0};
|
||||
|
||||
if (!isDelete)
|
||||
{
|
||||
if (tx.isFieldPresent(sfDelaySeconds))
|
||||
delay = tx.getFieldU32(sfDelaySeconds);
|
||||
if (tx.isFieldPresent(sfRepeatCount))
|
||||
recur = tx.getFieldU32(sfRepeatCount);
|
||||
if (tx.isFieldPresent(sfStartTime))
|
||||
{
|
||||
startTime = tx.getFieldU32(sfStartTime);
|
||||
if (startTime == 0)
|
||||
startTime = view.parentCloseTime().time_since_epoch().count();
|
||||
}
|
||||
}
|
||||
|
||||
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(startTime, 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(sfStartTime, startTime);
|
||||
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);
|
||||
|
||||
if (tx.isFlag(tfCronUnset))
|
||||
// delete cron
|
||||
return baseFee;
|
||||
|
||||
auto const repeatCount =
|
||||
tx.isFieldPresent(sfRepeatCount) ? tx.getFieldU32(sfRepeatCount) : 0;
|
||||
|
||||
// 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 = 1 + repeatCount;
|
||||
auto const additionalFee = baseFee * additionalExpectedExecutions;
|
||||
|
||||
if (baseFee + additionalFee < baseFee)
|
||||
return baseFee;
|
||||
|
||||
return baseFee + additionalFee;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
56
src/ripple/app/tx/impl/SetCron.h
Normal file
56
src/ripple/app/tx/impl/SetCron.h
Normal 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
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -410,6 +410,9 @@ 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;
|
||||
extern SF_UINT32 const sfStartTime;
|
||||
|
||||
// 64-bit integers (common)
|
||||
extern SF_UINT64 const sfIndexNext;
|
||||
@@ -486,6 +489,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, it’s 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
|
||||
|
||||
@@ -68,6 +68,7 @@ LedgerFormats::LedgerFormats()
|
||||
{sfGovernanceMarks, soeOPTIONAL},
|
||||
{sfAccountIndex, soeOPTIONAL},
|
||||
{sfTouchCount, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
@@ -366,6 +367,17 @@ LedgerFormats::LedgerFormats()
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Cron,
|
||||
ltCRON,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfStartTime, soeREQUIRED},
|
||||
{sfDelaySeconds, soeREQUIRED},
|
||||
{sfRepeatCount, soeREQUIRED},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,9 @@ CONSTRUCT_TYPED_SFIELD(sfLockCount, "LockCount", UINT32,
|
||||
|
||||
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
|
||||
|
||||
CONSTRUCT_TYPED_SFIELD(sfStartTime, "StartTime", UINT32, 93);
|
||||
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 +242,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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -472,6 +472,23 @@ TxFormats::TxFormats()
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Cron,
|
||||
ttCRON,
|
||||
{
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::CronSet,
|
||||
ttCRON_SET,
|
||||
{
|
||||
{sfDelaySeconds, soeOPTIONAL},
|
||||
{sfRepeatCount, soeOPTIONAL},
|
||||
{sfStartTime, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
496
src/test/app/Cron_test.cpp
Normal file
496
src/test/app/Cron_test.cpp
Normal file
@@ -0,0 +1,496 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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);
|
||||
|
||||
// CLAIM
|
||||
env(cron::set(alice),
|
||||
cron::startTime(0),
|
||||
cron::repeat(100),
|
||||
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
|
||||
auto expected = baseFee * 2 + baseFee * 256;
|
||||
env(cron::set(alice),
|
||||
cron::startTime(0),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
fee(expected - 1),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
|
||||
env(cron::set(alice),
|
||||
cron::startTime(0),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
cron::repeat(256),
|
||||
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 DelaySeconds and RepeatCount and StartTime are not
|
||||
// specified
|
||||
env(cron::set(alice), ter(temMALFORMED));
|
||||
|
||||
// Invalid DelaySeconds and RepeatCount combination with StartTime
|
||||
env(cron::set(alice),
|
||||
cron::startTime(100),
|
||||
cron::delay(356 * 24 * 60 * 60),
|
||||
ter(temMALFORMED));
|
||||
env(cron::set(alice),
|
||||
cron::startTime(100),
|
||||
cron::repeat(256),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid DelaySeconds
|
||||
env(cron::set(alice),
|
||||
cron::startTime(100),
|
||||
cron::delay(365 * 24 * 60 * 60 + 1),
|
||||
cron::repeat(256),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid RepeatCount
|
||||
env(cron::set(alice),
|
||||
cron::startTime(100),
|
||||
cron::delay(365 * 24 * 60 * 60),
|
||||
cron::repeat(257),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// Invalid with tfCronUnset flag
|
||||
env(cron::set(alice),
|
||||
cron::delay(365 * 24 * 60 * 60),
|
||||
txflags(tfCronUnset),
|
||||
ter(temMALFORMED));
|
||||
env(cron::set(alice),
|
||||
cron::repeat(100),
|
||||
txflags(tfCronUnset),
|
||||
ter(temMALFORMED));
|
||||
env(cron::set(alice),
|
||||
cron::startTime(100),
|
||||
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};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// Past StartTime
|
||||
env(cron::set(alice),
|
||||
cron::startTime(
|
||||
env.timeKeeper().now().time_since_epoch().count() - 1),
|
||||
fee(XRP(1)),
|
||||
ter(tecEXPIRED));
|
||||
env.close();
|
||||
|
||||
// Too far Future StartTime
|
||||
env(cron::set(alice),
|
||||
cron::startTime(
|
||||
env.timeKeeper().now().time_since_epoch().count() +
|
||||
365 * 24 * 60 * 60 + 1),
|
||||
fee(XRP(1)),
|
||||
ter(tecEXPIRED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
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
|
||||
auto parentCloseTime =
|
||||
env.current()->parentCloseTime().time_since_epoch().count();
|
||||
env(cron::set(alice),
|
||||
cron::startTime(parentCloseTime + 356 * 24 * 60 * 60),
|
||||
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);
|
||||
BEAST_EXPECT(
|
||||
cronSle->getFieldU32(sfStartTime) ==
|
||||
parentCloseTime + 356 * 24 * 60 * 60);
|
||||
|
||||
// update cron
|
||||
parentCloseTime =
|
||||
env.current()->parentCloseTime().time_since_epoch().count();
|
||||
env(cron::set(alice),
|
||||
cron::startTime(0),
|
||||
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);
|
||||
BEAST_EXPECT(cronSle2->getFieldU32(sfStartTime) == parentCloseTime);
|
||||
|
||||
// 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::startTime(baseTime + 100),
|
||||
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(10s);
|
||||
|
||||
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 = baseTime + 100;
|
||||
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::startTime(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
|
||||
@@ -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,107 @@ 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::startTime(0),
|
||||
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();
|
||||
|
||||
auto const baseTime =
|
||||
env.current()->parentCloseTime().time_since_epoch().count();
|
||||
// cron set
|
||||
env(cron::set(account),
|
||||
cron::startTime(baseTime + 100),
|
||||
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 +6512,8 @@ private:
|
||||
testURITokenCancelSellOfferTSH(features);
|
||||
testURITokenCreateSellOfferTSH(features);
|
||||
testRemitTSH(features);
|
||||
testCronSetTSH(features);
|
||||
testCronTSH(features);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -357,6 +357,32 @@ class UNLReport_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(isImportVL(env, ivlKeys[0]) == true);
|
||||
BEAST_EXPECT(isImportVL(env, ivlKeys[1]) == false);
|
||||
BEAST_EXPECT(isActiveValidator(env, vlKeys[0]) == true);
|
||||
|
||||
// now test unrecognised keys that are already present in the ledger
|
||||
// object (flap fix)
|
||||
l = std::make_shared<Ledger>(
|
||||
*l, env.app().timeKeeper().closeTime());
|
||||
|
||||
// insert a ttUNL_REPORT pseudo into the open ledger
|
||||
env.app().openLedger().modify(
|
||||
[&](OpenView& view, beast::Journal j) -> bool {
|
||||
STTx tx = createUNLRTx(l->seq(), ivlKeys[1], vlKeys[0]);
|
||||
uint256 txID = tx.getTransactionID();
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
tx.add(*s);
|
||||
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
return true;
|
||||
});
|
||||
|
||||
BEAST_EXPECT(hasUNLReport(env) == true);
|
||||
|
||||
// close the ledger
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(isImportVL(env, ivlKeys[0]) == true);
|
||||
BEAST_EXPECT(isImportVL(env, ivlKeys[1]) == false);
|
||||
BEAST_EXPECT(isActiveValidator(env, vlKeys[0]) == true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,4 +1350,4 @@ createUNLRTx(
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
} // namespace ripple
|
||||
|
||||
@@ -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>
|
||||
|
||||
89
src/test/jtx/cron.h
Normal file
89
src/test/jtx/cron.h
Normal file
@@ -0,0 +1,89 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 StartTime on a JTx. */
|
||||
class startTime
|
||||
{
|
||||
private:
|
||||
uint32_t startTime_;
|
||||
|
||||
public:
|
||||
explicit startTime(uint32_t startTime) : startTime_(startTime)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(Env&, JTx& jtx) const;
|
||||
};
|
||||
|
||||
/** 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
|
||||
62
src/test/jtx/impl/cron.cpp
Normal file
62
src/test/jtx/impl/cron.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
startTime::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
jt.jv[sfStartTime.jsonName] = startTime_;
|
||||
}
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user