From ac1bf8859622d40c08784f51913ca7593047511a Mon Sep 17 00:00:00 2001 From: tequ Date: Wed, 15 Oct 2025 15:14:59 +0900 Subject: [PATCH] add tests for CronSet --- Builds/CMake/RippledCore.cmake | 2 + src/ripple/app/tx/impl/InvariantCheck.cpp | 1 + src/ripple/app/tx/impl/SetCron.cpp | 40 ++-- src/ripple/app/tx/impl/SetCron.h | 1 - src/test/app/Cron_test.cpp | 278 ++++++++++++++++++++++ src/test/jtx.h | 1 + src/test/jtx/cron.h | 74 ++++++ src/test/jtx/impl/cron.cpp | 56 +++++ 8 files changed, 427 insertions(+), 26 deletions(-) create mode 100644 src/test/app/Cron_test.cpp create mode 100644 src/test/jtx/cron.h create mode 100644 src/test/jtx/impl/cron.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 41b0c6faf..b0423b02b 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -736,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 @@ -900,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 diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 231828a82..5679a2b47 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -491,6 +491,7 @@ LedgerEntryTypesMatch::visitEntry( case ltNFTOKEN_PAGE: case ltNFTOKEN_OFFER: case ltURI_TOKEN: + case ltCRON: case ltIMPORT_VLSEQ: case ltUNL_REPORT: break; diff --git a/src/ripple/app/tx/impl/SetCron.cpp b/src/ripple/app/tx/impl/SetCron.cpp index 6246fb745..6a0c56a49 100644 --- a/src/ripple/app/tx/impl/SetCron.cpp +++ b/src/ripple/app/tx/impl/SetCron.cpp @@ -19,12 +19,9 @@ #include #include -#include #include #include #include -#include -#include #include #include @@ -75,7 +72,7 @@ SetCron::preflight(PreflightContext const& ctx) auto delay = tx.getFieldU32(sfDelaySeconds); if (delay > 31536000UL /* 365 days in seconds */) { - JLOG(j.debug()) << "SetCron: DelaySeconds was too high. (max 14 " + JLOG(j.debug()) << "SetCron: DelaySeconds was too high. (max 365 " "days in seconds)."; return temMALFORMED; } @@ -99,18 +96,6 @@ SetCron::preflight(PreflightContext const& ctx) TER SetCron::preclaim(PreclaimContext const& ctx) { - if (!ctx.view.rules().enabled(featureCron)) - return temDISABLED; - - auto& j = ctx.j; - - auto const id = ctx.tx[sfAccount]; - - auto const sle = ctx.view.read(keylet::account(id)); - - if (!sle) - return terNO_ACCOUNT; - return tesSUCCESS; } @@ -244,22 +229,27 @@ SetCron::doApply() XRPAmount SetCron::calculateBaseFee(ReadView const& view, STTx const& tx) { - auto fee = Transactor::calculateBaseFee(view, tx); + auto const baseFee = Transactor::calculateBaseFee(view, tx); + + auto const hasRepeat = tx.isFieldPresent(sfRepeatCount); + auto const hasDelay = tx.isFieldPresent(sfDelaySeconds); + + if (!hasRepeat && !hasDelay) + // 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 recur = tx.isFieldPresent(sfRepeatCount) - ? tx.getFieldU32(sfRepeatCount) + 2 - : 2; + uint32_t const additionalExpectedExecutions = + tx.getFieldU32(sfRepeatCount) + 1; + auto const additionalFee = baseFee * additionalExpectedExecutions; - auto finalFee = fee * recur; + if (baseFee + additionalFee < baseFee) + return baseFee; - if (finalFee < fee) - return fee; - - return finalFee; + return baseFee + additionalFee; } } // namespace ripple diff --git a/src/ripple/app/tx/impl/SetCron.h b/src/ripple/app/tx/impl/SetCron.h index 4a899fe57..3f255ffc7 100644 --- a/src/ripple/app/tx/impl/SetCron.h +++ b/src/ripple/app/tx/impl/SetCron.h @@ -22,7 +22,6 @@ #include #include -#include #include namespace ripple { diff --git a/src/test/app/Cron_test.cpp b/src/test/app/Cron_test.cpp new file mode 100644 index 000000000..1f69a6292 --- /dev/null +++ b/src/test/app/Cron_test.cpp @@ -0,0 +1,278 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include "ripple/protocol/Indexes.h" +#include "ripple/protocol/TER.h" +#include "ripple/protocol/TxFlags.h" +#include "test/jtx/TestHelpers.h" +#include "test/jtx/cron.h" +#include + +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}) + { + // If the BalanceRewards amendment is not enabled, you should not be + // able to claim rewards. + 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), 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), fee(expected - 1), ter(telINSUF_FEE_P)); + env.close(); + + env(cron::set(alice), 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 + // can have flag 1 set to opt-out of rewards + { + env(cron::set(alice), txflags(tfClose), ter(temINVALID_FLAG)); + env(cron::set(alice), + txflags(tfUniversalMask), + ter(temINVALID_FLAG)); + } + + // temMALFORMED + { + // Invalid DelaySeconds and RepeatCount combination + // (only RepeatCount specified) + env(cron::set(alice), cron::repeat(256), ter(temMALFORMED)); + env.close(); + + // Invalid DelaySeconds + env(cron::set(alice), + cron::delay(365 * 24 * 60 * 60 + 1), + cron::repeat(256), + ter(temMALFORMED)); + env.close(); + + // Invalid RepeatCount + env(cron::set(alice), + cron::delay(365 * 24 * 60 * 60), + cron::repeat(257), + ter(temMALFORMED)); + env.close(); + } + } + + void + testInvalidPreclaim(FeatureBitset features) + { + testcase("invalid preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // no preclaim checks exists + 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)), 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)); + } + + void + testWithFeats(FeatureBitset features) + { + testEnabled(features); + testFee(features); + testInvalidPreflight(features); + testInvalidPreclaim(features); + testDoApply(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 diff --git a/src/test/jtx.h b/src/test/jtx.h index 0220cbed0..b46b41e47 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/cron.h b/src/test/jtx/cron.h new file mode 100644 index 000000000..e8ccc9ad4 --- /dev/null +++ b/src/test/jtx/cron.h @@ -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 +#include + +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 diff --git a/src/test/jtx/impl/cron.cpp b/src/test/jtx/impl/cron.cpp new file mode 100644 index 000000000..26fd8cf6e --- /dev/null +++ b/src/test/jtx/impl/cron.cpp @@ -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 +#include + +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::SetCron; + 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