Compare commits

...

7 Commits

Author SHA1 Message Date
Nicholas Dudfield
3a18dbcc53 feat: add NuDB block size tests and improve error handling
Incorporates changes from XRPLF/rippled PR #5468:

- Add getBlockSize() method to Backend interface
  Returns std::optional<std::size_t> for backends that support
  configurable block sizes (currently only NuDB)

- Update parseBlockSize() to throw exceptions instead of warnings
  Invalid block size configurations now throw std::runtime_error
  with descriptive error messages instead of silently using defaults

- Read existing database block size as default
  parseBlockSize() now checks existing nudb.key file for block size
  using nudb::block_size() before applying configuration

- Add comprehensive unit tests (NuDBFactory_test.cpp)
  Tests cover default sizes, valid sizes (4K-32K power-of-2),
  invalid sizes, error messages, power-of-2 validation, and
  data persistence across different block sizes

These changes improve configurability and error reporting while
maintaining backward compatibility.
2025-10-22 10:15:00 +07:00
Valon Mamudi
d82f74576e added missing include 2025-10-22 09:49:00 +07:00
Valon Mamudi
8d377199ce added configurable NuDB block size support in xahaud 2025-10-22 09:49:00 +07:00
Alloy Networks
9378f1a0ad Update CONTRIBUTING.md (#599) 2025-10-21 14:20:10 +10:00
tequ
6fa6a96e3a Introduce StartTime in CronSet and improve next execution scheduling (#596) 2025-10-21 14:17:53 +10:00
RichardAH
b0fcd36bcd import_vl_keys logic fix (flap fix) (#588) 2025-10-18 16:27:05 +10:00
RichardAH
1ec31e79c9 Cron (on ledger cronjobs) (#590)
Co-authored-by: tequ <git@tequ.dev>
2025-10-17 18:45:16 +10:00
37 changed files with 2121 additions and 36 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
@@ -961,6 +965,7 @@ if (tests)
src/test/nodestore/Basics_test.cpp
src/test/nodestore/DatabaseShard_test.cpp
src/test/nodestore/Database_test.cpp
src/test/nodestore/NuDBFactory_test.cpp
src/test/nodestore/Timing_test.cpp
src/test/nodestore/import_test.cpp
src/test/nodestore/varint_test.cpp

View File

@@ -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

View File

@@ -1127,6 +1127,39 @@
# it must be defined with the same value in both
# sections.
#
# Optional keys for NuDB only:
#
# nudb_block_size EXPERIMENTAL: Block size in bytes for NuDB storage.
# Must be a power of 2 between 4096 and 32768. Default is 4096.
#
# This parameter controls the fundamental storage unit
# size for NuDB's internal data structures. The choice
# of block size can significantly impact performance
# depending on your storage hardware and filesystem:
#
# - 4096 bytes: Optimal for most standard SSDs and
# traditional filesystems (ext4, NTFS, HFS+).
# Provides good balance of performance and storage
# efficiency. Recommended for most deployments.
#
# - 8192-16384 bytes: May improve performance on
# high-end NVMe SSDs and copy-on-write filesystems
# like ZFS or Btrfs that benefit from larger block
# alignment. Can reduce metadata overhead for large
# databases.
#
# - 32768 bytes (32K): Maximum supported block size
# for high-performance scenarios with very fast
# storage. May increase memory usage and reduce
# efficiency for smaller databases.
#
# Note: This setting cannot be changed after database
# creation without rebuilding the entire database.
# Choose carefully based on your hardware and expected
# database size.
#
# Example: nudb_block_size=4096
#
# These keys modify the behavior of online_delete, and thus are only
# relevant if online_delete is defined and non-zero:

View File

@@ -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)

View File

@@ -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

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

@@ -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;
}

View 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

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,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

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

@@ -23,6 +23,7 @@
#include <ripple/nodestore/Types.h>
#include <atomic>
#include <cstdint>
#include <optional>
namespace ripple {
namespace NodeStore {
@@ -175,6 +176,14 @@ public:
virtual int
fdRequired() const = 0;
/** Get the block size for backends that support it
*/
virtual std::optional<std::size_t>
getBlockSize() const
{
return std::nullopt;
}
/** Returns read and write stats.
@note The Counters struct is specific to and only used

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <ripple/basics/contract.h>
#include <ripple/beast/core/LexicalCast.h>
#include <ripple/nodestore/Factory.h>
#include <ripple/nodestore/Manager.h>
#include <ripple/nodestore/impl/DecodedBlob.h>
@@ -31,6 +32,7 @@
#include <exception>
#include <memory>
#include <nudb/nudb.hpp>
#include <sstream>
namespace ripple {
namespace NodeStore {
@@ -48,6 +50,7 @@ public:
size_t const keyBytes_;
std::size_t const burstSize_;
std::string const name_;
std::size_t const blockSize_;
nudb::store db_;
std::atomic<bool> deletePath_;
Scheduler& scheduler_;
@@ -62,6 +65,7 @@ public:
, keyBytes_(keyBytes)
, burstSize_(burstSize)
, name_(get(keyValues, "path"))
, blockSize_(parseBlockSize(name_, keyValues, journal))
, deletePath_(false)
, scheduler_(scheduler)
{
@@ -81,6 +85,7 @@ public:
, keyBytes_(keyBytes)
, burstSize_(burstSize)
, name_(get(keyValues, "path"))
, blockSize_(parseBlockSize(name_, keyValues, journal))
, db_(context)
, deletePath_(false)
, scheduler_(scheduler)
@@ -110,6 +115,12 @@ public:
return name_;
}
std::optional<std::size_t>
getBlockSize() const override
{
return blockSize_;
}
void
open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt)
override
@@ -137,7 +148,7 @@ public:
uid,
salt,
keyBytes_,
nudb::block_size(kp),
blockSize_,
0.50,
ec);
if (ec == nudb::errc::file_exists)
@@ -362,6 +373,56 @@ public:
{
return 3;
}
private:
static std::size_t
parseBlockSize(
std::string const& name,
Section const& keyValues,
beast::Journal journal)
{
using namespace boost::filesystem;
auto const folder = path(name);
auto const kp = (folder / "nudb.key").string();
std::size_t const defaultSize =
nudb::block_size(kp); // Default 4K from NuDB
std::size_t blockSize = defaultSize;
std::string blockSizeStr;
if (!get_if_exists(keyValues, "nudb_block_size", blockSizeStr))
{
return blockSize; // Early return with default
}
try
{
std::size_t const parsedBlockSize =
beast::lexicalCastThrow<std::size_t>(blockSizeStr);
// Validate: must be power of 2 between 4K and 32K
if (parsedBlockSize < 4096 || parsedBlockSize > 32768 ||
(parsedBlockSize & (parsedBlockSize - 1)) != 0)
{
std::stringstream s;
s << "Invalid nudb_block_size: " << parsedBlockSize
<< ". Must be power of 2 between 4096 and 32768.";
Throw<std::runtime_error>(s.str());
}
JLOG(journal.info())
<< "Using custom NuDB block size: " << parsedBlockSize
<< " bytes";
return parsedBlockSize;
}
catch (std::exception const& e)
{
std::stringstream s;
s << "Invalid nudb_block_size value: " << blockSizeStr
<< ". Error: " << e.what();
Throw<std::runtime_error>(s.str());
}
}
};
//------------------------------------------------------------------------------

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,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;

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,17 @@ LedgerFormats::LedgerFormats()
},
commonFields);
add(jss::Cron,
ltCRON,
{
{sfOwner, soeREQUIRED},
{sfStartTime, soeREQUIRED},
{sfDelaySeconds, soeREQUIRED},
{sfRepeatCount, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
},
commonFields);
// clang-format on
}

View File

@@ -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);

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,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&

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;

496
src/test/app/Cron_test.cpp Normal file
View 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

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,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

View File

@@ -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

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>

89
src/test/jtx/cron.h Normal file
View 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

View 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

View File

@@ -0,0 +1,357 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
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 <test/nodestore/TestBase.h>
#include <test/unit_test/SuiteJournal.h>
#include <ripple/basics/BasicConfig.h>
#include <ripple/basics/ByteUtilities.h>
#include <ripple/beast/utility/temp_dir.h>
#include <ripple/nodestore/DummyScheduler.h>
#include <ripple/nodestore/Manager.h>
#include <memory>
#include <sstream>
namespace ripple {
namespace NodeStore {
class NuDBFactory_test : public TestBase
{
private:
// Helper function to create a Section with specified parameters
Section
createSection(std::string const& path, std::string const& blockSize = "")
{
Section params;
params.set("type", "nudb");
params.set("path", path);
if (!blockSize.empty())
params.set("nudb_block_size", blockSize);
return params;
}
// Helper function to create a backend and test basic functionality
bool
testBackendFunctionality(
Section const& params,
std::size_t expectedBlocksize)
{
try
{
DummyScheduler scheduler;
test::SuiteJournal journal("NuDBFactory_test", *this);
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
if (!BEAST_EXPECT(backend))
return false;
if (!BEAST_EXPECT(backend->getBlockSize() == expectedBlocksize))
return false;
backend->open();
if (!BEAST_EXPECT(backend->isOpen()))
return false;
// Test basic store/fetch functionality
auto batch = createPredictableBatch(10, 12345);
storeBatch(*backend, batch);
Batch copy;
fetchCopyOfBatch(*backend, &copy, batch);
backend->close();
return areBatchesEqual(batch, copy);
}
catch (...)
{
return false;
}
}
// Helper function to test log messages
void
testLogMessage(
Section const& params,
beast::severities::Severity level,
std::string const& expectedMessage)
{
test::StreamSink sink(level);
beast::Journal journal(sink);
DummyScheduler scheduler;
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
std::string logOutput = sink.messages().str();
BEAST_EXPECT(logOutput.find(expectedMessage) != std::string::npos);
}
public:
void
testDefaultBlockSize()
{
testcase("Default block size (no nudb_block_size specified)");
beast::temp_dir tempDir;
auto params = createSection(tempDir.path());
// Should work with default 4096 block size
BEAST_EXPECT(testBackendFunctionality(params, 4096));
}
void
testValidBlockSizes()
{
testcase("Valid block sizes");
std::vector<std::size_t> validSizes = {4096, 8192, 16384, 32768};
for (auto const& size : validSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), to_string(size));
BEAST_EXPECT(testBackendFunctionality(params, size));
}
}
void
testInvalidBlockSizes()
{
testcase("Invalid block sizes");
std::vector<std::string> invalidSizes = {
"2048", // Too small
"1024", // Too small
"65536", // Too large
"131072", // Too large
"5000", // Not power of 2
"6000", // Not power of 2
"10000", // Not power of 2
"0", // Zero
"-1", // Negative
"abc", // Non-numeric
"4k", // Invalid format
"4096.5" // Decimal
};
for (auto const& size : invalidSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
DummyScheduler scheduler;
test::SuiteJournal journal("NuDBFactory_test", *this);
// Should throw exception for invalid sizes
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
// If we get here, the test failed
BEAST_EXPECT(false);
}
catch (std::exception const& e)
{
// Expected exception
std::string error{e.what()};
BEAST_EXPECT(
error.find("Invalid nudb_block_size") != std::string::npos);
}
}
}
void
testLogMessages()
{
testcase("Log message verification");
// Test valid custom block size logging
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "8192");
testLogMessage(
params,
beast::severities::kInfo,
"Using custom NuDB block size: 8192");
}
// Test invalid block size exception message
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "5000");
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
fail();
}
catch (std::exception const& e)
{
std::string logOutput{e.what()};
BEAST_EXPECT(
logOutput.find("Invalid nudb_block_size: 5000") !=
std::string::npos);
BEAST_EXPECT(
logOutput.find(
"Must be power of 2 between 4096 and 32768") !=
std::string::npos);
}
}
// Test non-numeric value exception message
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), "invalid");
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
fail();
}
catch (std::exception const& e)
{
std::string logOutput{e.what()};
BEAST_EXPECT(
logOutput.find("Invalid nudb_block_size value: invalid") !=
std::string::npos);
}
}
}
void
testPowerOfTwoValidation()
{
testcase("Power of 2 validation logic");
// Test edge cases around valid range
std::vector<std::pair<std::string, bool>> testCases = {
{"4095", false}, // Just below minimum
{"4096", true}, // Minimum valid
{"4097", false}, // Just above minimum, not power of 2
{"8192", true}, // Valid power of 2
{"8193", false}, // Just above valid power of 2
{"16384", true}, // Valid power of 2
{"32768", true}, // Maximum valid
{"32769", false}, // Just above maximum
{"65536", false} // Power of 2 but too large
};
for (auto const& [size, shouldWork] : testCases)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
test::StreamSink sink(beast::severities::kWarning);
beast::Journal journal(sink);
DummyScheduler scheduler;
try
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
BEAST_EXPECT(shouldWork);
}
catch (std::exception const& e)
{
std::string logOutput{e.what()};
BEAST_EXPECT(
logOutput.find("Invalid nudb_block_size") !=
std::string::npos);
BEAST_EXPECT(!shouldWork);
}
}
}
void
testDataPersistence()
{
testcase("Data persistence with different block sizes");
std::vector<std::string> blockSizes = {
"4096", "8192", "16384", "32768"};
for (auto const& size : blockSizes)
{
beast::temp_dir tempDir;
auto params = createSection(tempDir.path(), size);
DummyScheduler scheduler;
test::SuiteJournal journal("NuDBFactory_test", *this);
// Create test data
auto batch = createPredictableBatch(50, 54321);
// Store data
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
backend->open();
storeBatch(*backend, batch);
backend->close();
}
// Retrieve data in new backend instance
{
auto backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
backend->open();
Batch copy;
fetchCopyOfBatch(*backend, &copy, batch);
BEAST_EXPECT(areBatchesEqual(batch, copy));
backend->close();
}
}
}
void
run() override
{
testDefaultBlockSize();
testValidBlockSizes();
testInvalidBlockSizes();
testLogMessages();
testPowerOfTwoValidation();
testDataPersistence();
}
};
BEAST_DEFINE_TESTSUITE(NuDBFactory, ripple_core, ripple);
} // namespace NodeStore
} // namespace ripple