Compare commits

..

33 Commits

Author SHA1 Message Date
Ed Hennis
8442d018d4 Merge remote-tracking branch 'XRPLF/develop' into ximinez/emptydirectoryinvariant
* XRPLF/develop: (21 commits)
  fix: Re-store nodes missing from both backends during online_delete rotation (7763)
  fix: Add amendment sponsor for AccountRootsDeletedClean (7801)
  fix: Update base_uint and test changes released in 3.1.3 (7570)
  fix: Handle rounding just above kMaxRep more accurately (7389)
  fix: Document and assert "after" is never null in invariants (7354)
  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)
  ...
2026-07-14 21:20:51 -04:00
Ed Hennis
275f458e1e Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-07-09 19:49:56 -04:00
Ed Hennis
cf5f75d41a Merge remote-tracking branch 'XRPLF/develop' into ximinez/emptydirectoryinvariant
* XRPLF/develop: (137 commits)
  refactor: Retire InnerObjTemplate fix (7368)
  fix: Disable AMM creation with Vault shares (7666)
  test: Add tests for TMProofPathResponse and TMReplayDeltaResponse invalid hash/key sizes (7593)
  ci: [DEPENDABOT] bump actions/setup-python from 6.2.0 to 6.3.0 (7657)
  build: Don't reuse binaries between different C++ versions (7681)
  chore: Update pre-commit hooks && actions (7686)
  feat: Add an invariant to ensure object deletion also deletes its pseudo-account (7445)
  feat: Add Batch (XLS-56) V1_1 (6446)
  feat: Introduce lending 1.1 amendment and add `MemoData` field to `VaultDelete` transaction (6324)
  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)
  ...
2026-07-01 16:41:16 -04:00
Ed Hennis
6270c9c850 Merge remote-tracking branch 'XRPLF/develop' into ximinez/emptydirectoryinvariant
* XRPLF/develop: (30 commits)
  chore: Pin Python packages for codegen using uv (7329)
  style: Use shfmt instead of bashate (7326)
  fix: Fix edge-case where vault-depositor may get stuck (7139)
  fix: Fix `VaultInvariant` and `VaultDeposit` precision bugs at IOU scale boundaries (7272)
  ci: Add clang to nix images (7308)
  fix: Include management-fee delta in doOverpayment assertion (7039)
  fix: Fix clang-tidy pre-commit hook to locate compile_commands.json from repo root (7325)
  fix: Use consistent scale for `debtTotal` (7093)
  fix: Skip deleted book directories and non-root modifications in `ValidBookDirectory` invariant (7312)
  fix: Address review feedback on FD/handle guarding (5823 follow-up) (7310)
  fix: Fix non-canonical MPT amount (7117)
  release: Bump version to 3.2.0-b7 (7316)
  fix: Check if the MPT first loss cover can be sent to the broker before deleting the broker (7125)
  fix: Fix RPM prerelease ordering and start xrpld on DEB install (7313)
  ci: Re-enable full nproc for Linux (7315)
  fix: Add assorted MPT/DEX fixes (7040)
  refactor: Remove dead `fetchBatch` code (7309)
  release: Bump version to 3.2.0-b6 (7311)
  chore: Revert graceful peer disconnection and follow-up fix (7296)
  fix: Fix IOU precision issues in LoanBrokerCover transactions (7274)
  ...
2026-05-26 16:55:33 -04:00
Ed Hennis
90ea49b9f9 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-19 16:54:04 -04:00
Ed Hennis
ba35a9fb20 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-19 10:15:45 -04:00
Ed Hennis
0ef677f007 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-19 09:50:43 -04:00
Ed Hennis
a8f75ce155 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-14 20:39:33 -04:00
Ed Hennis
1cb6fd15b1 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-14 10:48:49 -04:00
Ed Hennis
ef59b14fb7 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-13 12:04:09 -04:00
Ed Hennis
4d2dc5fa54 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-12 20:11:38 -04:00
Ed Hennis
5b81a40bf1 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-07 18:10:30 -04:00
Ed Hennis
713dcaacb0 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-07 14:19:38 -04:00
Ed Hennis
b6b367e290 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-07 13:28:59 -04:00
Ed Hennis
50501c89d1 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-06 22:34:50 -04:00
Ed Hennis
dacc664f90 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-06 14:18:29 -04:00
Ed Hennis
410322362d Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-05 19:08:36 -04:00
Ed Hennis
588aa5c420 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-05-01 14:01:09 -04:00
Ed Hennis
f4a37fb3a4 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-28 16:31:44 -04:00
Ed Hennis
5faacf6006 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-25 14:46:10 -04:00
Ed Hennis
cabee3faac Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-23 15:56:30 -04:00
Ed Hennis
7ed2258782 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-22 23:52:49 -04:00
Ed Hennis
f97b4d01fb Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-22 14:49:30 -04:00
Ed Hennis
e657df5fe1 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-22 13:11:01 -04:00
Ed Hennis
ca190b5aaa Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-21 19:35:04 -04:00
Ed Hennis
503014f03f Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-20 17:50:03 -04:00
Ed Hennis
29f5829680 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-20 15:45:21 -04:00
Ed Hennis
2b1f7f9d55 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-20 11:39:26 -04:00
Ed Hennis
c776515cee Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-17 18:21:03 -04:00
Ed Hennis
5d9b00dba4 Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-16 13:44:53 -04:00
Ed Hennis
a81d37465e Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-15 19:09:37 -04:00
Ed Hennis
e341af4aee Merge branch 'develop' into ximinez/emptydirectoryinvariant 2026-04-15 14:29:12 -04:00
Ed Hennis
8122ed62b6 Experiment: Add invariant to enforce directory node population
- Experiment: Always delete the root
2026-04-13 19:58:50 -04:00
25 changed files with 376 additions and 433 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

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

@@ -394,6 +394,22 @@ public:
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: An account's directory should never be empty
*
*/
class NoEmptyDirectory
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* Verify that MPT/XRP STAmounts are canonical in any ledger entries left after the
* transaction applies.
@@ -463,7 +479,9 @@ using InvariantChecks = std::tuple<
ValidMPTTransfer,
ObjectHasPseudoAccount,
SponsorshipOwnerCountsMatch,
SponsorshipAccountCountMatchesField>;
SponsorshipAccountCountMatchesField,
ObjectHasPseudoAccount,
NoEmptyDirectory>;
/**
* @brief get a tuple of all invariant checks

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

@@ -255,6 +255,7 @@ ApplyView::emptyDirDelete(Keylet const& directory)
bool
ApplyView::dirRemove(Keylet const& directory, std::uint64_t page, uint256 const& key, bool keepRoot)
{
keepRoot = false;
auto node = peek(keylet::page(directory, page));
if (!node)

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

@@ -1206,6 +1206,46 @@ NoModifiedUnmodifiableFields::finalize(
return true;
}
//------------------------------------------------------------------------------
void
NoEmptyDirectory::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
return;
if (before && before->getType() != ltDIR_NODE)
return;
if (after && after->getType() != ltDIR_NODE)
return;
if (!after->isFieldPresent(sfOwner))
// Not an account dir
return;
bad_ = after->at(sfIndexes).empty();
}
bool
NoEmptyDirectory::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (bad_)
{
JLOG(j.fatal()) << "Invariant failed: empty owner directory.";
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
ValidAmounts::visitEntry(
bool isDelete,

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