Compare commits

..

39 Commits

Author SHA1 Message Date
Ed Hennis
3c31a1dd01 Merge branch 'develop' into ximinez/fix/validator-cache 2026-07-14 20:13:13 -04:00
Ed Hennis
bbfaf85a14 Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix/validator-cache
* XRPLF/develop:
  ci: Run full matrix only on `Ready to merge` or `Full CI build` labeled PRs (7689)
  fix: Strengthen Clawback invariant checks for MPT balances (7285)
  test: Add unit tests for IP address related functions (7744)
  ci: Add Rust to Nix docker image (7571)
  docs: Add more information about pre-commit hooks and how to set them up (7802)
  test: Add JSON array size tests (7592)
  chore: Enable most readability checks (7772)
  ci: Do not run conflict checker when label is applied (7774)
  chore: Run clang_tidy_check with `pass_filenames: false` from pre-commit (7800)
  feat: Add delegate filter param for account_tx RPC (6126)
  refactor: Move `jss.h` `include` out of `Indexes.h` (7799)
  test: Add tests for check doxygen style (7795)
  style: Add pre-commit hook to check doxygen style (7794)
  style: Unify style for all Doxygen comments (7776)
  fix: Improve Number addition/subtraction rounding (7369)
  feat: XLS-68: Sponsor, 5887 continuation  (7350)
2026-07-14 15:02:07 -04:00
Ed Hennis
1c4c998ae1 Merge branch 'develop' into ximinez/fix/validator-cache 2026-07-09 19:50:05 -04:00
Ed Hennis
5d2faef541 Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix/validator-cache
* XRPLF/develop: (158 commits)
  chore: Use std::ranges where possible (7634)
  ci: Use macOS 26 Tahoe with apple-clang 21 (7601)
  build: Mark sec256k1 and mpt-crypto as transitive headers (7658)
  chore: Add a script to nicely format clang-tidy output (7650)
  chore: Enable most bugprone checks (7643)
  feat: Confidential Transfer for MPT (5860)
  fix: Use trustline balance direction to validate IOU PaymentMint/PaymentBurn (7584)
  fix: Unify freeze checks for pseudo-account deposit/withdraw (7382)
  fix: Block delegate tx from being queued (7640)
  chore: Enable groups of clang-tidy checks by default (7637)
  ci: Better determine when we need to run full clang-tidy (7635)
  refactor: Retire NFTokenReserve fix (7367)
  refactor: Retire Clawback amendment (7353)
  refactor: Rename (mostly keylet) functions to more closely match the docs (7059)
  build: Switch to a new conan XRPLF remote, again (7638)
  chore: Revert "build: Switch to a new conan XRPLF remote (7622)" (7623)
  build: Switch to a new conan XRPLF remote (7622)
  build: Align xrpld RPM packaging with DEB package (7529)
  chore: Use clang-tidy v22 new features (7427)
  build: Patch nix binaries in CMake (7539)
  ...
2026-06-30 16:48:26 -04:00
Ed Hennis
5d2be06748 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-19 16:53:46 -04:00
Ed Hennis
2f5b17c2cb Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-19 10:15:25 -04:00
Ed Hennis
a963e4a389 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-19 05:16:02 -04:00
Ed Hennis
962f3ee744 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-15 21:43:29 -04:00
Ed Hennis
2a7499a415 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-14 20:39:19 -04:00
Ed Hennis
ae8c44b606 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-13 22:32:02 -04:00
Ed Hennis
1edd6fe4bf Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-13 12:03:49 -04:00
Ed Hennis
2edf310a6f Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-12 19:18:57 -04:00
Ed Hennis
7af68be818 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-12 16:26:19 -04:00
Ed Hennis
19fc5c48dd Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-11 13:34:22 -04:00
Ed Hennis
2f43a1901d Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-07 18:10:13 -04:00
Ed Hennis
6a0440bf6d Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-07 14:18:39 -04:00
Ed Hennis
cf398d05aa Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-07 13:28:42 -04:00
Ed Hennis
59a8e30a3e Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-06 22:34:33 -04:00
Ed Hennis
8160921783 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-06 14:18:12 -04:00
Ed Hennis
6dc01196b4 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-05 16:46:54 -04:00
Ed Hennis
cdbbbdd27f Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-05 10:50:44 -04:00
Ed Hennis
3d30c09031 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-05 00:14:58 -04:00
Ed Hennis
e59ca23487 Merge branch 'develop' into ximinez/fix/validator-cache 2026-05-01 13:59:15 -04:00
Ed Hennis
72c700c3e0 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-28 16:23:38 -04:00
Ed Hennis
4589fcbcfc Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-25 14:45:54 -04:00
Ed Hennis
86d840f53d Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-23 15:56:11 -04:00
Ed Hennis
741b61cdf3 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-22 23:40:28 -04:00
Ed Hennis
6bb0989c9f Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-22 14:49:12 -04:00
Ed Hennis
9120506613 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-22 13:10:44 -04:00
Ed Hennis
3b3de96bd4 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-21 18:48:39 -04:00
Ed Hennis
c9ab6ab25f Merge remote-tracking branch 'XRPLF/develop' into ximinez/fix/validator-cache
* XRPLF/develop:
  chore: Remove empty Taker.h (6984)
  chore: Enable clang-tidy modernize checks (6975)
  ci: Upload clang-tidy git diff (6983)
  fix: Add rounding to Vault invariants (6217) (6955)
2026-04-21 18:46:58 -04:00
Ed Hennis
fb0605cfd3 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-20 17:49:47 -04:00
Ed Hennis
156553bb5e Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-20 15:45:04 -04:00
Ed Hennis
781b56849b Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-20 11:39:06 -04:00
Ed Hennis
278c02bebb Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-17 18:11:31 -04:00
Ed Hennis
1d6fedf9a2 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-16 13:44:37 -04:00
Ed Hennis
2e8de499aa Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-15 19:06:29 -04:00
Ed Hennis
0bce3639a6 Merge branch 'develop' into ximinez/fix/validator-cache 2026-04-15 14:28:55 -04:00
Ed Hennis
8f329e3bc6 Use Validator List (VL) cache files in more scenarios
- If any [validator_list_keys] are not available after all
  [validator_list_sites] have had a chance to be queried, then fall
  back to loading cache files. Currently, cache files are only used if
  no sites are defined, or the request to one of them has an error. It
  does not include cases where not enough sites are defined, or if a
  site returns an invalid VL (or something else entirely).
- Resolves #5320
2026-04-13 19:46:06 -04:00
27 changed files with 339 additions and 526 deletions

View File

@@ -25,11 +25,18 @@ on:
- unlabeled
concurrency:
# A single per-ref group with cancel-in-progress means any newer run (a push
# or a label change) supersedes the in-progress one for that ref. Keeping
# exactly one authoritative run per ref ensures a fast do-nothing run can never
# mask a real build's checks.
group: ${{ github.workflow }}-${{ github.ref }}
# Use a per-ref group so a newer run (a push, or a change to a label below)
# supersedes the in-progress one for that ref. Label events we don't act on get
# their own unique group (per run id) instead, keeping them out of the shared
# group so real builds keep running. Keep this list in sync with `should-run`.
group: >-
${{ github.workflow }}-${{ github.ref }}${{
((github.event.action == 'labeled' || github.event.action == 'unlabeled')
&& github.event.label.name != 'Ready to merge'
&& github.event.label.name != 'DraftRunCI'
&& github.event.label.name != 'Full CI build')
&& format('-{0}', github.run_id) || ''
}}
cancel-in-progress: true
defaults:
@@ -37,17 +44,21 @@ defaults:
shell: bash
jobs:
# This job determines whether the rest of the workflow should run at all,
# based on the current set of labels: it runs when the PR is not a draft
# (which should also cover merge-group) or has the 'DraftRunCI' or
# 'Full CI build' label. Whether a build then happens, and whether it is the
# minimal or full matrix, is decided further below and in the strategy matrix.
# This job determines whether the rest of the workflow should run. It runs
# when the PR is not a draft (which should also cover merge-group) or has the
# 'DraftRunCI' or 'Full CI build' label. For label events it only runs when the
# label added or removed is one we act on ('Ready to merge', 'DraftRunCI' or
# 'Full CI build'), so unrelated label changes do not trigger a redundant run.
should-run:
if: >-
${{
!github.event.pull_request.draft
|| contains(github.event.pull_request.labels.*.name, 'DraftRunCI')
|| contains(github.event.pull_request.labels.*.name, 'Full CI build')
((github.event.action != 'labeled' && github.event.action != 'unlabeled')
|| github.event.label.name == 'Ready to merge'
|| github.event.label.name == 'DraftRunCI'
|| github.event.label.name == 'Full CI build')
&& (!github.event.pull_request.draft
|| contains(github.event.pull_request.labels.*.name, 'DraftRunCI')
|| contains(github.event.pull_request.labels.*.name, 'Full CI build'))
}}
runs-on: ubuntu-latest
steps:

View File

@@ -10,22 +10,22 @@
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
"protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
"openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288",
"openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
"mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355",
"mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828",
"lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188",
"libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732",
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
"grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135",
"grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
"c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732",
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"build_requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
@@ -38,7 +38,7 @@
"b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
"abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047"
"abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
],
"python_requires": [],
"overrides": {

View File

@@ -134,7 +134,7 @@ class Xrpl(ConanFile):
if self.options.jemalloc:
self.requires("jemalloc/5.3.1")
self.requires("lz4/1.10.0", force=True)
self.requires("mpt-crypto/0.4.0-rc4", transitive_headers=True)
self.requires("mpt-crypto/0.4.0-rc2", transitive_headers=True)
self.requires("protobuf/6.33.5", force=True)
if self.options.rocksdb:
self.requires("rocksdb/10.5.1")

View File

@@ -3,9 +3,6 @@
#include <xrpl/basics/IntrusivePointer.ipp>
#include <xrpl/basics/Log.h> // IWYU pragma: keep
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/scope.h>
#include <algorithm>
namespace xrpl {
@@ -604,42 +601,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
std::vector<key_type> v;
{
// Keep track of how many iterations are needed. Exit the loop if the number of retries gets
// absurd. (Note that if this somehow ever happens, one more allocation will be done under
// lock, which is undesirable, but really should be almost impossible.)
std::size_t allocationIterations = 0;
std::unique_lock lock(mutex_);
for (auto size = cache_.size(); v.capacity() < size && allocationIterations < 20;
size = cache_.size())
{
ScopeUnlock const unlock(lock);
if (allocationIterations > 0)
{
JLOG(journal_.info())
<< "getKeys(): Cache grew beyond allocated capacity after "
<< allocationIterations << " prior attempt(s). Have " << v.capacity()
<< ", need " << size << ". Retrying allocation";
}
// Allocate the current size plus a little extra, in case the cache grows while
// allocating. Each time another allocation is needed, the extra also gets bigger until
// it ultimately doubles the size + 1.
constexpr std::size_t baseShift = 5;
auto const bufferOffset = std::min(allocationIterations, std::size_t{baseShift});
auto const bufferShift = baseShift - bufferOffset;
size += (size >> bufferShift) + 1;
v.reserve(size);
++allocationIterations;
}
if (v.capacity() < cache_.size())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::TaggedCache::getKeys(): failed to allocate sufficient capacity");
v.reserve(cache_.size());
// LCOV_EXCL_STOP
}
XRPL_ASSERT(lock.owns_lock(), "xrpl::TaggedCache::getKeys(): owns lock");
XRPL_ASSERT(
v.capacity() >= cache_.size(), "xrpl::TaggedCache::getKeys(): sufficient capacity");
std::scoped_lock const lock(mutex_);
v.reserve(cache_.size());
for (auto const& _ : cache_)
v.push_back(_.first);
}

View File

@@ -41,19 +41,6 @@ public:
std::unique_ptr<NodeStore::Backend>&& newBackend,
std::function<void(std::string const& writableName, std::string const& archiveName)> const&
f) = 0;
/**
* Marks an online-delete rotation as in progress (or completed).
*
* While in flight, a read served by the archive backend is copied
* forward into the writable backend even for ordinary
* (duplicate == false) fetches: the archive is about to be deleted,
* and a node body canonicalized into caches during the rotation
* window would otherwise survive only in RAM once the archive is
* dropped.
*/
virtual void
setRotationInFlight(bool inFlight) = 0;
};
} // namespace xrpl::NodeStore

View File

@@ -9,7 +9,6 @@
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
@@ -70,22 +69,11 @@ public:
void
sweep() override;
void
setRotationInFlight(bool inFlight) override;
private:
std::shared_ptr<Backend> writableBackend_;
std::shared_ptr<Backend> archiveBackend_;
mutable std::mutex mutex_;
// True between SHAMapStore starting the cache-freshen phase and the
// completion of rotate(). While true, archive hits on ordinary
// (duplicate == false) fetches are copied forward into the writable
// backend; copyForwardCount_ tallies them per rotation for the
// summary line logged at swap.
std::atomic<bool> rotationInFlight_{false};
std::atomic<std::uint64_t> copyForwardCount_{0};
std::shared_ptr<NodeObject>
fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate)
override;

View File

@@ -229,6 +229,13 @@ public:
[[nodiscard]] AccountID
getAccountID(SField const& field) const;
/**
* The account responsible for the authorization: the delegate when
* sfDelegate is present, otherwise the account.
*/
[[nodiscard]] AccountID
getInitiator() const;
[[nodiscard]] Blob
getFieldVL(SField const& field) const;
[[nodiscard]] STAmount const&

View File

@@ -142,26 +142,9 @@ public:
TxnSql status,
std::string const& escapedMetaData) const;
/**
* The IDs of the inner transactions of a Batch.
*/
[[nodiscard]] std::vector<uint256>
[[nodiscard]] std::vector<uint256> const&
getBatchTransactionIDs() const;
/**
* The inner transactions of a Batch, built and validated at construction.
* Always seated for Batch STTx instances (construction throws if oversized).
*/
[[nodiscard]] std::vector<std::shared_ptr<STTx const>> const&
getBatchTransactions() const;
/**
* The account responsible for the authorization: the delegate when
* sfDelegate is present, otherwise the account.
*/
[[nodiscard]] AccountID
getInitiator() const;
[[nodiscard]] AccountID
getFeePayerID() const;
@@ -183,16 +166,13 @@ private:
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const;
checkBatchSingleSign(STObject const& batchSigner) const;
[[nodiscard]] std::expected<void, std::string>
checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const;
checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
void
buildBatchTxns();
buildBatchTxnIds();
STBase*
copy(std::size_t n, void* buf) const override;
@@ -200,11 +180,11 @@ private:
move(std::size_t n, void* buf) override;
friend class detail::STVar;
std::optional<std::vector<std::shared_ptr<STTx const>>> batchTxns_;
std::optional<std::vector<uint256>> batchTxnIds_;
};
bool
passesLocalChecks(STTx const& tx, std::string&);
passesLocalChecks(STObject const& st, std::string&);
/**
* Sterilize a transaction.

View File

@@ -12,7 +12,6 @@
#include <array>
#include <cstdint>
#include <optional>
namespace xrpl {
@@ -40,9 +39,6 @@ public:
static NotTEC
checkSign(PreclaimContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
@@ -80,10 +76,6 @@ private:
// only be reached through Batch::checkSign.
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
// nullopt on overflow or oversized signer arrays.
static std::optional<XRPAmount>
calculateBaseFeeImpl(ReadView const& view, STTx const& tx);
};
} // namespace xrpl

View File

@@ -13,7 +13,6 @@
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <atomic>
#include <cstdint>
#include <exception>
#include <functional>
@@ -53,7 +52,6 @@ DatabaseRotatingImp::rotate(
// callback finishes. Only then will the archive directory be
// deleted.
std::shared_ptr<NodeStore::Backend> oldArchiveBackend;
std::uint64_t copyForwards = 0;
{
std::scoped_lock const lock(mutex_);
@@ -64,28 +62,11 @@ DatabaseRotatingImp::rotate(
newArchiveBackendName = archiveBackend_->getName();
writableBackend_ = std::move(newBackend);
copyForwards = copyForwardCount_.exchange(0, std::memory_order_relaxed);
}
if (copyForwards > 0)
{
JLOG(j_.warn()) << "Rotating: copied forward " << copyForwards
<< " archive-served reads into the writable backend "
"during the rotation window";
}
f(newWritableBackendName, newArchiveBackendName);
}
void
DatabaseRotatingImp::setRotationInFlight(bool inFlight)
{
rotationInFlight_.store(inFlight, std::memory_order_release);
JLOG(j_.debug()) << "Rotating: copy-forward on archive reads "
<< (inFlight ? "enabled" : "disabled");
}
std::string
DatabaseRotatingImp::getName() const
{
@@ -196,18 +177,9 @@ DatabaseRotatingImp::fetchNodeObject(
writable = writableBackend_;
}
// Update writable backend with data from the archive backend.
// While a rotation is in flight, ordinary (duplicate == false)
// reads served by the archive are copied forward too: the
// archive is about to be deleted, and a body canonicalized
// into the cache after the freshen getKeys() snapshot would
// otherwise survive only in RAM once the archive is dropped.
if (duplicate || rotationInFlight_.load(std::memory_order_acquire))
{
if (!duplicate)
copyForwardCount_.fetch_add(1, std::memory_order_relaxed);
// Update writable backend with data from the archive backend
if (duplicate)
writable->store(nodeObject);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace {
//------------------------------------------------------------------------------
// clang-format off
// NOLINTNEXTLINE(readability-identifier-naming)
char const* const versionString = "3.3.0-rc1"
char const* const versionString = "3.3.0-b1"
// clang-format on
;

View File

@@ -633,6 +633,20 @@ STObject::getAccountID(SField const& field) const
return getFieldByValue<STAccount>(field);
}
AccountID
STObject::getInitiator() const
{
// If sfDelegate is present, the delegate account is the initiator
// note: if a delegate is specified, its authorization to act on behalf of the account is
// enforced in `Transactor::invokeCheckPermission`
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
if (isFieldPresent(sfDelegate))
return getAccountID(sfDelegate);
// Default initiator
return getAccountID(sfAccount);
}
Blob
STObject::getFieldVL(SField const& field) const
{

View File

@@ -72,7 +72,7 @@ STTx::STTx(STObject&& object)
{
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
@@ -89,7 +89,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
applyTemplate(getTxFormat(txType_)->getSOTemplate()); // May throw
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfTransaction)
@@ -110,7 +110,7 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfT
logicError("Transaction type was mutated during assembly");
tid_ = getHash(HashPrefix::TransactionId);
buildBatchTxns();
buildBatchTxnIds();
}
STBase*
@@ -279,9 +279,12 @@ STTx::checkSign(Rules const& rules) const
return std::unexpected("Sponsor: " + ret.error());
}
// Verify batch signer signatures here so they are cached with the rest
// of signature checking.
if (isFieldPresent(sfBatchSigners))
// Verify the batch signer signatures here too, so they are cached with the
// rest of signature checking (checkValidity / SF_SIGGOOD) and stay out of
// the transaction engine. Gated on a batch (batchTxnIds_ seated) that
// actually carries signers; a batch whose inners are all from the outer
// account has no sfBatchSigners and needs no signer crypto.
if (batchTxnIds_ && isFieldPresent(sfBatchSigners))
{
if (auto const ret = checkBatchSign(rules); !ret)
return ret;
@@ -304,28 +307,11 @@ STTx::checkBatchSign(Rules const& rules) const
if (!isFieldPresent(sfBatchSigners))
return std::unexpected("Missing BatchSigners field."); // LCOV_EXCL_LINE
STArray const& signers{getFieldArray(sfBatchSigners)};
// Bound signature verification to the protocol cap. This runs in
// checkValidity (via checkSign) at relay / submit time, BEFORE preflight
// and passesLocalChecks enforce the cap. Without this guard a malicious
// peer could put an oversized sfBatchSigners array in a 1 MB blob and
// force one signature verification per entry before any of those checks
// (or the fee charge) runs.
if (signers.size() > kMaxBatchSigners)
return std::unexpected("BatchSigners array exceeds max entries.");
// Defensive.
if (!batchTxns_)
{
// LCOV_EXCL_START
UNREACHABLE("STTx::checkBatchSign : batch transactions not built");
return std::unexpected("Missing inner transactions.");
// LCOV_EXCL_STOP
}
auto const txIds = getBatchTransactionIDs();
for (auto const& signer : signers)
{
Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules, txIds)
: checkBatchSingleSign(signer, txIds);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules)
: checkBatchSingleSign(signer);
if (!result)
return result;
@@ -455,11 +441,12 @@ STTx::checkSingleSign(STObject const& sigObject) const
}
std::expected<void, std::string>
STTx::checkBatchSingleSign(STObject const& batchSigner, std::vector<uint256> const& txIds) const
STTx::checkBatchSingleSign(STObject const& batchSigner) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction");
Serializer msg;
serializeBatch(msg, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
serializeBatch(
msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice());
}
@@ -542,10 +529,7 @@ multiSignHelper(
}
std::expected<void, std::string>
STTx::checkBatchMultiSign(
STObject const& batchSigner,
Rules const& rules,
std::vector<uint256> const& txIds) const
STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
// We can ease the computational load inside the loop a bit by
@@ -553,7 +537,8 @@ STTx::checkBatchMultiSign(
// with the stuff that stays constant from signature to signature.
auto const batchSignerAccount = batchSigner.getAccountID(sfAccount);
Serializer dataStart;
serializeBatch(dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds);
serializeBatch(
dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
dataStart.addBitString(batchSignerAccount);
return multiSignHelper(
batchSigner,
@@ -592,79 +577,38 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
}
void
STTx::buildBatchTxns()
STTx::buildBatchTxnIds()
{
// Precondition: the template must have been applied first, so the fields
// (including sfRawTransactions) are canonical before the inner txns are
// hashed. The constructors call this immediately after applying the
// template; isFree() being false confirms a template is set.
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxns : template applied");
if (getTxnType() != ttBATCH)
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template applied");
if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions))
return;
// A Batch always seats its inner transactions here, so every downstream
// consumer can rely on them. sfRawTransactions is required by the format
// (applyTemplate rejects a Batch without it); this guards a future change
// that made it optional.
if (!isFieldPresent(sfRawTransactions))
{
// LCOV_EXCL_START
UNREACHABLE("STTx::buildBatchTxns : missing RawTransactions");
Throw<std::runtime_error>("Batch has no RawTransactions.");
// LCOV_EXCL_STOP
}
auto const& raw = getFieldArray(sfRawTransactions);
if (raw.size() > kMaxBatchTxCount)
Throw<std::runtime_error>("Batch has too many inner transactions.");
// Build and validate each inner as an STTx once. A malformed inner throws;
// a nested batch is rejected before building it (a batch cannot contain a
// batch, and building one would recurse).
auto& txns = batchTxns_.emplace();
txns.reserve(raw.size());
// Seated for any batch with raw transactions. The count is validated in
// preflight and at the relay boundary, so build every id here; this keeps
// the invariant batchTxnIds_->size() == rawTransactions.size().
auto& ids = batchTxnIds_.emplace();
ids.reserve(raw.size());
for (STObject const& rb : raw)
{
if (rb.getFieldU16(sfTransactionType) == ttBATCH)
Throw<std::runtime_error>("Batch inner transaction cannot be a Batch.");
txns.push_back(std::make_shared<STTx const>(STObject{rb}));
}
ids.push_back(rb.getHash(HashPrefix::TransactionId));
}
std::vector<uint256>
std::vector<uint256> const&
STTx::getBatchTransactionIDs() const
{
auto const& txns = getBatchTransactions();
std::vector<uint256> ids;
ids.reserve(txns.size());
for (auto const& stx : txns)
ids.push_back(stx->getTransactionID());
return ids;
}
std::vector<std::shared_ptr<STTx const>> const&
STTx::getBatchTransactions() const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactions : batch transaction");
XRPL_ASSERT(batchTxns_.has_value(), "STTx::getBatchTransactions : batch transactions built");
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction");
XRPL_ASSERT(
batchTxns_->size() == getFieldArray(sfRawTransactions).size(),
"STTx::getBatchTransactions : batch transactions size mismatch");
return *batchTxns_;
}
AccountID
STTx::getInitiator() const
{
// If sfDelegate is present, the delegate account is the initiator
// note: if a delegate is specified, its authorization to act on behalf of the account is
// enforced in `Transactor::invokeCheckPermission`
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
if (isFieldPresent(sfDelegate))
return getAccountID(sfDelegate);
// Default initiator
return getAccountID(sfAccount);
batchTxnIds_.has_value(), "STTx::getBatchTransactionIDs : batch transaction IDs built");
XRPL_ASSERT(
batchTxnIds_->size() == getFieldArray(sfRawTransactions).size(),
"STTx::getBatchTransactionIDs : batch transaction IDs size mismatch");
// NOLINTNEXTLINE(bugprone-unchecked-optional-access): guarded by assert above
return *batchTxnIds_;
}
AccountID
@@ -810,62 +754,86 @@ invalidMPTAmountInTx(STObject const& tx)
}
static bool
isBatchRawTransactionOkay(STTx const& tx, std::string& reason)
isBatchRawTransactionOkay(STObject const& st, std::string& reason)
{
if (!tx.isFieldPresent(sfRawTransactions))
if (!st.isFieldPresent(sfRawTransactions))
return true;
// sfRawTransactions only appears on a Batch. passesLocalChecks runs on
// unverified user and peer input, so reject (rather than assert) a non-batch
// transaction that carries it.
if (tx.getTxnType() != ttBATCH)
if (st.getFieldU16(sfTransactionType) != ttBATCH)
{
reason = "Only Batch transactions may contain raw transactions.";
return false;
}
if (tx.isFieldPresent(sfBatchSigners) &&
tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
if (st.isFieldPresent(sfBatchSigners) &&
st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
{
reason = "BatchSigners array exceeds max entries.";
reason = "Batch Signers array exceeds max entries.";
return false;
}
// Inner structure (type, template, no nesting, count) is validated when the
// batch STTx is constructed; here we only run each inner's local checks.
for (auto const& inner : tx.getBatchTransactions())
auto const& rawTxns = st.getFieldArray(sfRawTransactions);
if (rawTxns.size() > kMaxBatchTxCount)
{
if (!passesLocalChecks(*inner, reason))
reason = "Raw Transactions array exceeds max entries.";
return false;
}
for (STObject raw : rawTxns)
{
try
{
auto const tt = safeCast<TxType>(raw.getFieldU16(sfTransactionType));
if (tt == ttBATCH)
{
reason = "Raw Transactions may not contain batch transactions.";
return false;
}
raw.applyTemplate(getTxFormat(tt)->getSOTemplate());
// passesLocalChecks recurses back into isBatchRawTransactionOkay,
// but an inner can never be a batch (rejected above), so the
// recursion terminates at depth 1.
if (!passesLocalChecks(raw, reason))
return false;
}
catch (std::exception const& e)
{
reason = e.what();
return false;
}
}
return true;
}
bool
passesLocalChecks(STTx const& tx, std::string& reason)
passesLocalChecks(STObject const& st, std::string& reason)
{
if (!isMemoOkay(tx, reason))
if (!isMemoOkay(st, reason))
return false;
if (!isAccountFieldOkay(tx))
if (!isAccountFieldOkay(st))
{
reason = "An account field is invalid.";
return false;
}
if (isPseudoTx(tx))
if (isPseudoTx(st))
{
reason = "Cannot submit pseudo transactions.";
return false;
}
if (invalidMPTAmountInTx(tx))
if (invalidMPTAmountInTx(st))
{
reason = "Amount can not be MPT.";
return false;
}
if (!isBatchRawTransactionOkay(tx, reason))
if (!isBatchRawTransactionOkay(st, reason))
return false;
return true;

View File

@@ -177,9 +177,9 @@ applyBatchTransactions(
int applied = 0;
for (auto const& stx : batchTxn.getBatchTransactions())
for (STObject rb : batchTxn.getFieldArray(sfRawTransactions))
{
auto const result = applyOneTransaction(*stx);
auto const result = applyOneTransaction(STTx{std::move(rb)});
XRPL_ASSERT(
result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)),
"Outer Batch failure, inner transaction should not be applied");

View File

@@ -1,12 +1,10 @@
#include <xrpl/tx/transactors/check/CheckCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -21,9 +19,6 @@ namespace xrpl {
NotTEC
CheckCancel::preflight(PreflightContext const& ctx)
{
if (ctx.rules.enabled(fixCleanup3_3_0) && ctx.tx[sfCheckID] == beast::kZero)
return temMALFORMED;
return tesSUCCESS;
}

View File

@@ -2,7 +2,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
@@ -52,9 +51,6 @@ CheckCash::checkExtraFeatures(xrpl::PreflightContext const& ctx)
NotTEC
CheckCash::preflight(PreflightContext const& ctx)
{
if (ctx.rules.enabled(fixCleanup3_3_0) && ctx.tx[sfCheckID] == beast::kZero)
return temMALFORMED;
// Exactly one of Amount or DeliverMin must be present.
auto const optAmount = ctx.tx[~sfAmount];
auto const optDeliverMin = ctx.tx[~sfDeliverMin];

View File

@@ -15,6 +15,7 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
@@ -27,7 +28,6 @@
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <utility>
@@ -46,11 +46,17 @@ namespace xrpl {
*
* @param view The ledger view providing fee and state information.
* @param tx The batch transaction to calculate the fee for.
* @return XRPAmount The total base fee required for the batch transaction,
* or std::nullopt on failure (overflow, oversized arrays).
* @return XRPAmount The total base fee required for the batch transaction.
*
* @throws std::overflow_error If any fee calculation would overflow the
* XRPAmount type.
* @throws std::length_error If the number of inner transactions or signers
* exceeds the allowed maximum.
* @throws std::invalid_argument If an inner transaction is itself a batch
* transaction.
*/
std::optional<XRPAmount>
Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
XRPAmount
Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount const maxAmount{std::numeric_limits<XRPAmount::value_type>::max()};
@@ -61,26 +67,49 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
if (baseFee > maxAmount - view.fees().base)
{
JLOG(debugLog().error()) << "BatchTrace: Base fee overflow detected.";
return std::nullopt;
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
XRPAmount const batchBase = view.fees().base + baseFee;
// Calculate the Inner Txn Fees. Inners are built and validated (count,
// no nesting) at construction, so they are reused here directly.
// Calculate the Inner Txn Fees
XRPAmount txnFees{0};
for (auto const& stx : tx.getBatchTransactions())
if (tx.isFieldPresent(sfRawTransactions))
{
auto const fee = xrpl::calculateBaseFee(view, *stx);
auto const& txns = tx.getFieldArray(sfRawTransactions);
// LCOV_EXCL_START
if (txnFees > maxAmount - fee)
if (txns.size() > kMaxBatchTxCount)
{
JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in txnFees calculation.";
return std::nullopt;
JLOG(debugLog().error()) << "BatchTrace: Raw Transactions array exceeds max entries.";
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
txnFees += fee;
for (STObject txn : txns)
{
STTx const stx = STTx{std::move(txn)};
// LCOV_EXCL_START
if (stx.getTxnType() == ttBATCH)
{
JLOG(debugLog().error()) << "BatchTrace: Inner Batch transaction found.";
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
auto const fee = xrpl::calculateBaseFee(view, stx);
// LCOV_EXCL_START
if (txnFees > maxAmount - fee)
{
JLOG(debugLog().error())
<< "BatchTrace: XRPAmount overflow in txnFees calculation.";
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
txnFees += fee;
}
}
// Calculate the Signers/BatchSigners Fees
@@ -93,7 +122,7 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
if (signers.size() > kMaxBatchSigners)
{
JLOG(debugLog().error()) << "BatchTrace: Batch Signers array exceeds max entries.";
return std::nullopt;
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
@@ -111,7 +140,7 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
{
JLOG(debugLog().error())
<< "BatchTrace: Nested Signers array exceeds max entries.";
return std::nullopt;
return kInitialXrp;
}
// LCOV_EXCL_STOP
signerCount += nestedSigners.size();
@@ -123,7 +152,7 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
if (signerCount > 0 && view.fees().base > maxAmount / signerCount)
{
JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerCount calculation.";
return std::nullopt;
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
@@ -133,13 +162,13 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
if (signerFees > maxAmount - txnFees)
{
JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerFees calculation.";
return std::nullopt;
return XRPAmount{kInitialXrp};
}
XRPAmount const innerFees = txnFees + signerFees;
if (innerFees > maxAmount - batchBase)
{
JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in total fee calculation.";
return std::nullopt;
return XRPAmount{kInitialXrp};
}
// LCOV_EXCL_STOP
@@ -147,24 +176,6 @@ Batch::calculateBaseFeeImpl(ReadView const& view, STTx const& tx)
return innerFees + batchBase;
}
XRPAmount
Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
{
if (auto const fee = calculateBaseFeeImpl(view, tx))
return *fee;
// The fee could not be computed, so return a placeholder the account can
// pay; preclaim rejects the transaction with tecINSUFF_FEE.
return view.fees().base; // LCOV_EXCL_LINE
}
TER
Batch::preclaim(PreclaimContext const& ctx)
{
if (!calculateBaseFeeImpl(ctx.view, ctx.tx))
return tecINSUFF_FEE; // LCOV_EXCL_LINE
return tesSUCCESS;
}
std::uint32_t
Batch::getFlagsMask(PreflightContext const& ctx)
{
@@ -235,6 +246,13 @@ Batch::preflight(PreflightContext const& ctx)
return temARRAY_EMPTY;
}
if (rawTxns.size() > kMaxBatchTxCount)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
<< "txns array exceeds 8 entries.";
return temARRAY_TOO_LARGE;
}
if (ctx.tx.isFieldPresent(sfBatchSigners) &&
ctx.tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
{
@@ -275,9 +293,9 @@ Batch::preflight(PreflightContext const& ctx)
return tesSUCCESS;
};
for (auto const& stxPtr : ctx.tx.getBatchTransactions())
for (STObject rb : rawTxns)
{
STTx const& stx = *stxPtr;
STTx const stx = STTx{std::move(rb)};
auto const hash = stx.getTransactionID();
if (!uniqueHashes.emplace(hash).second)
{
@@ -288,6 +306,14 @@ Batch::preflight(PreflightContext const& ctx)
}
auto const txType = stx.getFieldU16(sfTransactionType);
if (txType == ttBATCH)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "batch cannot have an inner batch txn. "
<< "txID: " << hash;
return temINVALID;
}
if (std::ranges::any_of(
kDisabledTxTypes, [txType](auto const& disabled) { return txType == disabled; }))
{
@@ -408,14 +434,15 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
ctx.tx.getTxnType() == ttBATCH, "xrpl::Batch::preflightSigValidated : batch transaction");
auto const parentBatchId = ctx.tx.getTransactionID();
auto const outerAccount = ctx.tx.getAccountID(sfAccount);
auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions);
// Accounts that must sign the batch: each inner authorizer and counterparty
// (excluding the outer account), sorted and de-duplicated to match against
// the ascending, unique batch signers.
std::vector<AccountID> requiredSigners;
requiredSigners.reserve(kMaxBatchSigners);
for (auto const& stxPtr : ctx.tx.getBatchTransactions())
for (STObject const& rb : rawTxns)
{
STTx const& rb = *stxPtr;
// A delegated inner is signed by the delegate, not the account holder,
// so the delegate is the required signer when present.
AccountID const authorizer = rb.getInitiator();

View File

@@ -4,6 +4,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
@@ -21,6 +22,9 @@ namespace xrpl {
NotTEC
ConfidentialMPTClawback::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
// Only issuer can clawback

View File

@@ -7,6 +7,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
@@ -25,6 +26,9 @@ namespace xrpl {
NotTEC
ConfidentialMPTConvert::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;

View File

@@ -6,6 +6,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
@@ -24,6 +25,9 @@ namespace xrpl {
NotTEC
ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot convert back
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;

View File

@@ -6,6 +6,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
@@ -23,6 +24,9 @@ namespace xrpl {
NotTEC
ConfidentialMPTMergeInbox::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
// issuer cannot merge
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
return temMALFORMED;

View File

@@ -31,6 +31,9 @@ ConfidentialMPTSend::checkExtraFeatures(PreflightContext const& ctx)
NotTEC
ConfidentialMPTSend::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();

View File

@@ -56,6 +56,7 @@
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
@@ -71,7 +72,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <map>
#include <memory>
#include <optional>
@@ -313,8 +313,8 @@ class Batch_test : public beast::unit_test::Suite
env.close();
}
// An oversized batch (more than kMaxBatchTxCount inners) fails STTx
// construction, so the transaction cannot be built.
// DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 9);
@@ -328,7 +328,7 @@ class Batch_test : public beast::unit_test::Suite
batch::Inner(pay(alice, bob, XRP(1)), seq + 7),
batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
batch::Inner(pay(alice, bob, XRP(1)), seq + 9),
Ter(temMALFORMED));
Ter(telENV_RPC_FAILED));
env.close();
}
@@ -345,15 +345,15 @@ class Batch_test : public beast::unit_test::Suite
env.close();
}
// A batch may not contain a batch: the nested inner fails STTx
// construction, so the transaction cannot be built.
// DEFENSIVE: temINVALID: Batch: batch cannot have inner batch txn.
// ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
{
auto const seq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
Ter(temMALFORMED));
Ter(telENV_RPC_FAILED));
env.close();
}
@@ -938,41 +938,80 @@ class Batch_test : public beast::unit_test::Suite
env.fund(XRP(10000), alice, bob);
// An inner missing a required field can no longer be submitted: the
// outer STTx builds and validates each inner at construction, so
// building the batch (as signing does) throws. Returns true if the
// build fails.
auto batchCtorFails = [&](json::StaticString const& field) -> bool {
// Invalid: sfTransactionType
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(field);
try
{
// Env::st swallows the construction failure and yields a null
// stx, so a malformed inner shows up as no transaction built.
auto const jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
return jt.stx == nullptr;
}
catch (std::exception const&)
{
return true;
}
};
tx1.removeMember(jss::TransactionType);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfTransactionType
BEAST_EXPECT(batchCtorFails(jss::TransactionType));
// Invalid: sfAccount
BEAST_EXPECT(batchCtorFails(jss::Account));
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Account);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfSequence
BEAST_EXPECT(batchCtorFails(jss::Sequence));
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Sequence);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfFee
BEAST_EXPECT(batchCtorFails(jss::Fee));
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::Fee);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Invalid: sfSigningPubKey
BEAST_EXPECT(batchCtorFails(jss::SigningPubKey));
{
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const seq = env.seq(alice);
auto tx1 = batch::Inner(pay(alice, bob, XRP(10)), seq + 1);
tx1.removeMember(jss::SigningPubKey);
auto jt = env.jtnofill(
batch::outer(alice, seq, batchFee, tfAllOrNothing),
tx1,
batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
env.close();
}
// Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2.
{
@@ -1473,13 +1512,11 @@ class Batch_test : public beast::unit_test::Suite
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
Ter(temMALFORMED));
Ter(telENV_RPC_FAILED));
env.close();
}
// An oversized batch (more than kMaxBatchTxCount inners) fails STTx
// construction, so it never reaches apply or checkValidity - Env::st
// swallows the failure and yields a null stx.
// temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
{
Env env{*this, features};
@@ -1502,44 +1539,11 @@ class Batch_test : public beast::unit_test::Suite
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq));
BEAST_EXPECT(jt.stx == nullptr);
}
// An oversized batch cannot slip through as validly signed even when it
// carries a BatchSigners array: the oversized inner array fails STTx
// construction before any signature is checked, so the batch can't be
// built at all (building it - as signing does - throws).
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const aliceSeq = env.seq(alice);
auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 9);
bool threw = false;
try
{
env.jtnofill(
batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Inner(pay(alice, bob, XRP(1)), aliceSeq),
batch::Sig(std::vector<Reg>(kMaxBatchSigners + 1, bob)));
}
catch (std::exception const&)
{
threw = true;
}
BEAST_EXPECT(threw);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
}
// Regression: the relay-boundary local check (isBatchRawTransactionOkay)
@@ -1594,14 +1598,6 @@ class Batch_test : public beast::unit_test::Suite
BEAST_EXPECT(!result.applied && result.ter == temARRAY_TOO_LARGE);
return result.applied;
});
// Regression (uncapped batch-signer verification): the relay
// boundary (checkValidity) rejects the oversized signers array via
// the checkBatchSign guard, BEFORE verifying a single signature.
auto const [valid, reason] =
xrpl::checkValidity(env.app().getHashRouter(), *jt.stx, env.current()->rules());
BEAST_EXPECT(valid == xrpl::Validity::SigBad);
BEAST_EXPECT(reason == "BatchSigners array exceeds max entries.");
}
}
@@ -5528,8 +5524,7 @@ class Batch_test : public beast::unit_test::Suite
return Batch::calculateBaseFee(*env.current(), *jtx.stx);
};
// bad: a batch may not contain a batch - the nested inner fails STTx
// construction, so the transaction cannot be built.
// bad: Inner Batch transaction found
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
@@ -5537,11 +5532,11 @@ class Batch_test : public beast::unit_test::Suite
batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::Inner(batch::outer(alice, seq, batchFee, tfAllOrNothing), seq),
batch::Inner(pay(alice, bob, XRP(1)), seq + 2));
BEAST_EXPECT(jtx.stx == nullptr);
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
}
// bad: an oversized batch (more than kMaxBatchTxCount inners) fails
// STTx construction, so it cannot be built.
// bad: Raw Transactions array exceeds max entries.
{
auto const seq = env.seq(alice);
XRPAmount const batchFee = batch::calcBatchFee(env, 0, 2);
@@ -5558,7 +5553,8 @@ class Batch_test : public beast::unit_test::Suite
batch::Inner(pay(alice, bob, XRP(1)), seq + 8),
batch::Inner(pay(alice, bob, XRP(1)), seq + 9));
BEAST_EXPECT(jtx.stx == nullptr);
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
}
// bad: Signers array exceeds max entries.
@@ -5571,9 +5567,8 @@ class Batch_test : public beast::unit_test::Suite
batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
batch::Sig(std::vector<Reg>(kMaxBatchSigners + 1, bob)));
// Failure paths fall back to the ledger base fee.
XRPAmount const txBaseFee = getBaseFee(jtx);
BEAST_EXPECT(txBaseFee == env.current()->fees().base);
BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
}
// good:

View File

@@ -1257,13 +1257,6 @@ class Check_test : public beast::unit_test::Suite
env.close();
}
// Can't run pre-amendment behavior due to assertion failure.
if (features[fixCleanup3_3_0])
{
env(check::cash(bob, uint256{}, usd(20)), Ter(temMALFORMED));
env.close();
}
// alice creates her checks ahead of time.
uint256 const chkIdU{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, bob, usd(20)));
@@ -1711,13 +1704,6 @@ class Check_test : public beast::unit_test::Suite
// Non-existent check.
env(check::cancel(bob, getCheckIndex(alice, env.seq(alice))), Ter(tecNO_ENTRY));
env.close();
// Can't run pre-amendment behavior due to assertion failure.
if (features[fixCleanup3_3_0])
{
env(check::cancel(bob, uint256{}), Ter(temMALFORMED));
env.close();
}
}
void

View File

@@ -21,7 +21,6 @@
#include <xrpl.pb.h>
#include <cstdint>
#include <cstring>
#include <exception>
#include <memory>
@@ -51,10 +50,15 @@ public:
run() override
{
testMalformedSerializedForm();
testcase("secp256k1 signatures");
testSTTx(KeyType::Secp256k1);
testcase("ed25519 signatures");
testSTTx(KeyType::Ed25519);
testcase("STObject constructor errors");
testObjectCtorErrors();
testBatchInnerCtorErrors();
}
void
@@ -1324,8 +1328,6 @@ public:
void
testSTTx(KeyType keyType)
{
testcase(std::string(to_string(keyType)) + " signatures");
auto const keypair = randomKeyPair(keyType);
STTx j(ttACCOUNT_SET, [&keypair](auto& obj) {
@@ -1380,8 +1382,6 @@ public:
void
testObjectCtorErrors()
{
testcase("STObject constructor errors");
auto const kp1 = randomKeyPair(KeyType::Secp256k1);
auto const id1 = calcAccountID(kp1.first);
@@ -1463,75 +1463,6 @@ public:
BEAST_EXPECT(got == "Field 'Fee' is required but missing.");
}
}
void
testBatchInnerCtorErrors()
{
testcase("Batch inner transaction validation");
auto const kp1 = randomKeyPair(KeyType::Secp256k1);
auto const id1 = calcAccountID(kp1.first);
auto const kp2 = randomKeyPair(KeyType::Secp256k1);
auto const id2 = calcAccountID(kp2.first);
// A raw inner transaction object of the given transaction type.
auto makeInner = [&](std::uint16_t txType) {
STObject inner(sfRawTransaction);
inner.setFieldU16(sfTransactionType, txType);
inner.setAccountID(sfAccount, id1);
inner.setAccountID(sfDestination, id2);
inner.setFieldAmount(sfAmount, STAmount(10000000000ull));
inner.setFieldAmount(sfFee, STAmount(0ull));
inner.setFieldU32(sfSequence, 1);
inner.setFieldVL(sfSigningPubKey, Slice(kp1.first.data(), kp1.first.size()));
return inner;
};
// An outer Batch STObject wrapping the given inner.
auto makeBatch = [&](STObject inner) {
STArray rawTxns(sfRawTransactions);
rawTxns.push_back(std::move(inner));
STObject batch(sfGeneric);
batch.setFieldU16(sfTransactionType, ttBATCH);
batch.setAccountID(sfAccount, id1);
batch.setFieldAmount(sfFee, STAmount(20ull));
batch.setFieldU32(sfSequence, 1);
batch.setFieldVL(sfSigningPubKey, Slice(kp1.first.data(), kp1.first.size()));
batch.setFieldArray(sfRawTransactions, rawTxns);
return batch;
};
{
// A batch whose inner is a well-formed transaction constructs.
std::string errorMsg;
try
{
STTx{makeBatch(makeInner(ttPAYMENT))};
}
catch (std::exception const& err)
{
errorMsg = err.what();
}
BEAST_EXPECT(errorMsg.empty());
}
{
// A batch whose inner carries an unregistered transaction type is
// rejected at construction, rather than surviving as a raw STObject
// and throwing later from an unprotected fee-calculation path.
std::string errorMsg;
try
{
STTx{makeBatch(makeInner(60000))};
}
catch (std::exception const& err)
{
errorMsg = err.what();
}
BEAST_EXPECT(matches(errorMsg.c_str(), "Invalid transaction type 60000"));
}
}
};
class InnerObjectFormatsSerializer_test : public beast::unit_test::Suite

View File

@@ -16,11 +16,9 @@
#include <xrpl/ledger/Ledger.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/Manager.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/detail/DatabaseRotatingImp.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/server/NetworkOPs.h>
#include <xrpl/server/State.h>
#include <xrpl/shamap/SHAMapMissingNode.h>
@@ -256,24 +254,8 @@ bool
SHAMapStoreImp::copyNode(std::uint64_t& nodeCount, SHAMapTreeNode const& node)
{
// Copy a single record from node to dbRotating_
auto obj = dbRotating_->fetchNodeObject(
dbRotating_->fetchNodeObject(
node.getHash().asUInt256(), 0, NodeStore::FetchType::Synchronous, true);
if (!obj)
{
XRPL_ASSERT(node.cowid() == 0, "SHAMapStoreImp::copyNode : rescued node must be clean");
// Reachable from the validated state map in memory, but present in
// neither backend: its only on-disk copy lived in a backend removed by
// an earlier rotation, and it was never rewritten because it is clean
// (cowid == 0, so flushDirty skips it). Persist the in-memory body
// directly into the writable backend so it survives this rotation
// instead of later surfacing as an unresolvable SHAMapMissingNode.
auto const hash = node.getHash().asUInt256();
Serializer s;
node.serializeWithPrefix(s);
dbRotating_->store(NodeObjectType::AccountNode, std::move(s.modData()), hash, 0);
JLOG(journal_.warn()) << "copyNode: re-stored node missing from both backends, hash="
<< hash << " type=" << static_cast<int>(node.getType());
}
if ((++nodeCount % checkHealthInterval_) == 0u)
{
if (healthWait() == HealthResult::Stopping)
@@ -366,23 +348,6 @@ SHAMapStoreImp::run()
JLOG(journal_.debug())
<< "copied ledger " << validatedSeq << " nodecount " << nodeCount;
// Close the getKeys()->swap exposure window: from here until
// rotate() completes, an ordinary read served by the archive is
// copied forward into the writable backend, so a node fetched
// from the doomed archive cannot be left RAM-only when the
// archive is deleted. RAII so the early returns below (and any
// exception) also clear the flag.
struct RotationExposureGuard
{
NodeStore::DatabaseRotating& db;
~RotationExposureGuard()
{
db.setRotationInFlight(false);
}
};
RotationExposureGuard const rotationExposureGuard{*dbRotating_};
dbRotating_->setRotationInFlight(true);
JLOG(journal_.debug()) << "freshening caches";
freshenCaches();
if (healthWait() == HealthResult::Stopping)

View File

@@ -160,7 +160,11 @@ ValidatorSite::load(
{
try
{
sites_.emplace_back(uri);
// This is not super efficient, but it doesn't happen often.
bool found = std::ranges::any_of(
sites_, [&uri](auto const& site) { return site.loadedResource->uri == uri; });
if (!found)
sites_.emplace_back(uri);
}
catch (std::exception const& e)
{
@@ -221,7 +225,17 @@ ValidatorSite::setTimer(
std::scoped_lock<std::mutex> const& siteLock,
std::scoped_lock<std::mutex> const& stateLock)
{
auto next = std::ranges::min_element(
if (!sites_.empty() && //
std::ranges::all_of(
sites_, [](auto const& site) { return site.lastRefreshStatus.has_value(); }))
{
// If all of the sites have been handled at least once (including
// errors and timeouts), call missingSite, which will load the cache
// files for any lists that are still unavailable.
missingSite(siteLock);
}
auto const next = std::ranges::min_element(
sites_, [](Site const& a, Site const& b) { return a.nextRefresh < b.nextRefresh; });
if (next != sites_.end())
@@ -332,7 +346,7 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec)
// processes a network error. Usually, this function runs first,
// but on extremely rare occasions, the response handler can run
// first, which will leave activeResource empty.
auto const& site = sites_[siteIdx];
auto& site = sites_[siteIdx];
if (site.activeResource)
{
JLOG(j_.warn()) << "Request for " << site.activeResource->uri << " took too long";
@@ -342,6 +356,9 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec)
JLOG(j_.error()) << "Request took too long, but a response has "
"already been processed";
}
if (!site.lastRefreshStatus)
site.lastRefreshStatus.emplace(
Site::Status{clock_type::now(), ListDisposition::Invalid, "timeout"});
}
std::scoped_lock const lockState{stateMutex_};