From 6341e752002f90dc87ed49650cb704eac5f62660 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 24 Jun 2026 13:15:11 +0100 Subject: [PATCH 001/100] refactor: Refactor TaggedCache.ipp to remove const_cast in canonicalize_replace_cache (#5638) Signed-off-by: JCW Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> --- include/xrpl/basics/TaggedCache.h | 86 +++++++++++++++++++-- include/xrpl/basics/TaggedCache.ipp | 87 +++++++++++++++++---- src/test/basics/TaggedCache_test.cpp | 110 +++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 19 deletions(-) diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h index 380b7c687f..ecf6071f8d 100644 --- a/include/xrpl/basics/TaggedCache.h +++ b/include/xrpl/basics/TaggedCache.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,22 @@ namespace xrpl { +namespace detail { + +// Replace-policy tags selecting how TaggedCache::canonicalizeImpl resolves a +// collision when the key already exists (defined in TaggedCache.ipp): +// - ReplaceCached: always replace the cached value with `data`. `data` is +// never written back and may be const. +// - ReplaceClient: keep the cached value and write it back into `data` (the +// client's pointer), which must therefore be writable. +// - ReplaceDynamically: call the supplied callback to decide per call; `data` +// is written back when the cached value is kept, so it must be writable. +struct ReplaceCached; +struct ReplaceClient; +struct ReplaceDynamically; + +} // namespace detail + /** Map/cache combination. This class implements a cache and a map. The cache keeps objects alive in the map. The map allows multiple code paths that reference objects @@ -96,6 +113,32 @@ public: bool del(key_type const& key, bool valid); +private: + // Selects the `data` parameter type of canonicalizeImpl from the replace + // policy: const for detail::ReplaceCached (never written back), otherwise + // writable. + template + using CanonicalizeClientPointerType = std::conditional_t< + std::is_same_v, + SharedPointerType const&, + SharedPointerType&>; + + /** Shared implementation of the canonicalize family. + + `policy` selects how a collision is resolved when `key` already exists: + detail::ReplaceCached, detail::ReplaceClient or + detail::ReplaceDynamically. For ReplaceDynamically `replaceCallback` is + invoked with the existing strong pointer and returns whether to replace + the cached value with `data`; for the tag policies it is unused. + */ + template + bool + canonicalizeImpl( + key_type const& key, + CanonicalizeClientPointerType data, + Policy policy, + Callback&& replaceCallback = nullptr); + public: /** Replace aliased objects with originals. @@ -104,19 +147,52 @@ public: This routine eliminates the duplicate and performs a replacement on the callers shared pointer if needed. + `replaceCallback` is a callable taking the existing strong pointer and + returning whether to replace the cached value with `data` (true) or to + keep the cached value and write it back into `data` (false). Because the + write-back case mutates `data`, `data` must be writable. + @param key The key corresponding to the object @param data A shared pointer to the data corresponding to the object. - @param replace Function that decides if cache should be replaced + @param replaceCallback A callable (existing strong pointer -> bool). - @return `true` If the key already existed. - */ - template + @return `true` if an existing live entry was found and used; `false` if a new entry was + inserted or an expired tracked entry was re-cached. + **/ + template bool - canonicalize(key_type const& key, SharedPointerType& data, R&& replaceCallback); + canonicalize(key_type const& key, SharedPointerType& data, Callback&& replaceCallback); + /** Insert/update the canonical entry for `key`, always replacing the + cached value with `data`. + + If an entry already exists for `key`, the cached value is unconditionally + replaced with `data`; otherwise `data` is inserted. `data` is never + written back, so it may be const. + + @param key The key corresponding to the object. + @param data A shared pointer to the data corresponding to the object. + + @return `true` if an existing live entry was found and used; `false` if a new entry was + inserted or an expired tracked entry was re-cached. + **/ bool canonicalizeReplaceCache(key_type const& key, SharedPointerType const& data); + /** Insert the canonical entry for `key`, keeping any existing cached value. + + If an entry already exists for `key`, the cached value is kept and + written back into `data` so the caller ends up with the canonical + object; otherwise `data` is inserted. Because `data` may be overwritten + it must be writable. + + @param key The key corresponding to the object. + @param data A shared pointer to the data corresponding to the object; + updated to the canonical value when one already exists. + + @return `true` if an existing live entry was found and used; `false` if a new entry was + inserted or an expired tracked entry was re-cached. + **/ bool canonicalizeReplaceClient(key_type const& key, SharedPointerType& data); diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index cee02749c6..6973ec4ba0 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -5,6 +5,30 @@ namespace xrpl { +namespace detail { + +// Replace-policy tags selecting how TaggedCache::canonicalizeImpl resolves a +// collision when the key already exists: +// - ReplaceCached: always replace the cached value with `data`. `data` is +// never written back and may be const. +// - ReplaceClient: keep the cached value and write it back into `data` (the +// client's pointer), which must therefore be writable. +// - ReplaceDynamically: call the supplied callback to decide per call; `data` +// is written back when the cached value is kept, so it must be writable. +struct ReplaceCached +{ +}; + +struct ReplaceClient +{ +}; + +struct ReplaceDynamically +{ +}; + +} // namespace detail + template < class Key, class T, @@ -300,13 +324,29 @@ template < class Hash, class KeyEqual, class Mutex> -template +template inline bool TaggedCache:: - canonicalize(key_type const& key, SharedPointerType& data, R&& replaceCallback) + canonicalizeImpl( + key_type const& key, + CanonicalizeClientPointerType data, + [[maybe_unused]] Policy policy, + [[maybe_unused]] Callback&& replaceCallback) { // Return canonical value, store if needed, refresh in cache // Return values: true=we had the data already + + // `Policy` is one of: + // - detail::ReplaceCached: always replace the cached value with `data`; + // `data` is never written back and may be const. + // - detail::ReplaceClient: keep the cached value and write it back into + // `data` (the client's pointer), which must therefore be writable. + // - detail::ReplaceDynamically: call `replaceCallback` to decide at run + // time; `data` must be writable. + // For the latter two the write-back below requires a mutable `data`, so + // passing a const argument is a compile error. + constexpr bool replaceCached = std::is_same_v; + std::scoped_lock const lock(mutex_); auto cit = cache_.find(key); @@ -324,13 +364,14 @@ TaggedCachesecond; entry.touch(clock_.now()); - auto shouldReplace = [&] { - if constexpr (std::is_invocable_r_v) + auto shouldReplaceCached = [&] { + if constexpr (replaceCached) { - // The reason for this extra complexity is for intrusive - // strong/weak combo getting a strong is relatively expensive - // and not needed for many cases. - return replaceCallback(); + return true; + } + else if constexpr (std::is_same_v) + { + return false; } else { @@ -340,11 +381,11 @@ TaggedCache +template +inline bool +TaggedCache:: + canonicalize(key_type const& key, SharedPointerType& data, Callback&& replaceCallback) +{ + return canonicalizeImpl( + key, data, detail::ReplaceDynamically{}, std::forward(replaceCallback)); +} + template < class Key, class T, @@ -389,7 +448,7 @@ inline bool TaggedCache:: canonicalizeReplaceCache(key_type const& key, SharedPointerType const& data) { - return canonicalize(key, const_cast(data), []() { return true; }); + return canonicalizeImpl(key, data, detail::ReplaceCached{}); } template < @@ -405,7 +464,7 @@ inline bool TaggedCache:: canonicalizeReplaceClient(key_type const& key, SharedPointerType& data) { - return canonicalize(key, data, []() { return false; }); + return canonicalizeImpl(key, data, detail::ReplaceClient{}); } template < diff --git a/src/test/basics/TaggedCache_test.cpp b/src/test/basics/TaggedCache_test.cpp index 77cd25e543..26564a4de8 100644 --- a/src/test/basics/TaggedCache_test.cpp +++ b/src/test/basics/TaggedCache_test.cpp @@ -1,5 +1,7 @@ #include +#include +#include #include #include // IWYU pragma: keep #include @@ -8,6 +10,7 @@ #include #include +#include namespace xrpl { @@ -133,6 +136,113 @@ public: BEAST_EXPECT(c.getCacheSize() == 0); BEAST_EXPECT(c.getTrackSize() == 0); } + { + BEAST_EXPECT(!c.insert(5, "five")); + BEAST_EXPECT(c.getCacheSize() == 1); + BEAST_EXPECT(c.size() == 1); + + { + auto const p1 = c.fetch(5); + BEAST_EXPECT(p1 != nullptr); + BEAST_EXPECT(c.getCacheSize() == 1); + BEAST_EXPECT(c.size() == 1); + + // Advance the clock a lot + ++clock; + c.sweep(); + BEAST_EXPECT(c.getCacheSize() == 0); + BEAST_EXPECT(c.size() == 1); + + auto p2 = std::make_shared("five_2"); + BEAST_EXPECT(c.canonicalizeReplaceCache(5, p2)); + BEAST_EXPECT(c.getCacheSize() == 1); + BEAST_EXPECT(c.size() == 1); + // Make sure the caller's original pointer is unchanged + BEAST_EXPECT(p1.get() != p2.get()); + BEAST_EXPECT(*p2 == "five_2"); + + auto const p3 = c.fetch(5); + BEAST_EXPECT(p3 != nullptr); + BEAST_EXPECT(p3.get() == p2.get()); + BEAST_EXPECT(p3.get() != p1.get()); + } + + ++clock; + c.sweep(); + BEAST_EXPECT(c.getCacheSize() == 0); + BEAST_EXPECT(c.size() == 0); + } + + { + testcase("intrptr"); + + struct MyRefCountObject : IntrusiveRefCounts + { + std::string data; + + // Needed to support weak intrusive pointers + virtual void + partialDestructor() {}; + + MyRefCountObject() = default; + explicit MyRefCountObject(std::string data) : data(std::move(data)) + { + } + + bool + operator==(std::string const& other) const + { + return data == other; + } + }; + + using IntrPtrCache = TaggedCache< + Key, + MyRefCountObject, + /*IsKeyCache*/ false, + intr_ptr::SharedWeakUnionPtr, + intr_ptr::SharedPtr>; + + IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal); + + intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared("one")); + BEAST_EXPECT(intrPtrCache.getCacheSize() == 1); + BEAST_EXPECT(intrPtrCache.size() == 1); + + { + { + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced")); + + auto p = intrPtrCache.fetch(1); + BEAST_EXPECT(*p == "one_replaced"); + + // Advance the clock a lot + ++clock; + intrPtrCache.sweep(); + BEAST_EXPECT(intrPtrCache.getCacheSize() == 0); + BEAST_EXPECT(intrPtrCache.size() == 1); + + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced_2")); + + auto p2 = intrPtrCache.fetch(1); + BEAST_EXPECT(*p2 == "one_replaced_2"); + + intrPtrCache.del(1, true); + } + + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced_3")); + auto p3 = intrPtrCache.fetch(1); + BEAST_EXPECT(*p3 == "one_replaced_3"); + } + + ++clock; + intrPtrCache.sweep(); + BEAST_EXPECT(intrPtrCache.getCacheSize() == 0); + BEAST_EXPECT(intrPtrCache.size() == 0); + } } }; From 69d289a388f7339470e861cee38b29a877ee26a3 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:15:45 -0400 Subject: [PATCH 002/100] fix: AMM Quality Leak into Domain BookStep for Permissioned DEX (#6853) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/libxrpl/tx/paths/BookStep.cpp | 5 ++ src/test/app/PermissionedDEX_test.cpp | 79 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index 5cc2a987b8..4b045521d2 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -905,6 +905,11 @@ BookStep::getAMMOffer( ReadView const& view, std::optional const& clobQuality) const { + // AMM doesn't support domain books. When fixCleanup3_3_0 is enabled, exclude + // AMM liquidity so quality estimation matches actual crossing (tryAMM skips + // AMM for domain books). + if (book_.domain && view.rules().enabled(fixCleanup3_3_0)) + return std::nullopt; if (ammLiquidity_) return ammLiquidity_->getOffer(view, clobQuality); return std::nullopt; diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 99e69ce482..51ca321f7e 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -993,6 +993,83 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(usd == USD(45)); } + void + testAmmQualityNotLeaked(FeatureBitset features) + { + bool const excludesAmmFromDomainQuality = features[fixCleanup3_3_0]; + + testcase << "AMM quality not leaked into domain BookStep" + << (excludesAmmFromDomainQuality ? " (Cleanup3_3_0 enabled)" + : " (Cleanup3_3_0 disabled)"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + auto const eur = gw["EUR"]; + + env.trust(eur(1000), bob, domainOwner); + env.close(); + env(pay(gw, bob, eur(100))); + env.close(); + + env(pay(gw, alice, USD(500))); + env.close(); + + // The AMM makes the direct XRP->USD book look much better than it + // really is for domain payments. The domain LOB direct path is 1:1, + // while the competing XRP->EUR->USD path is 2:1. + AMM const amm(env, alice, XRP(10), USD(500)); + + auto const directOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); + env.close(); + + auto const xrpEurOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), eur(20)), Domain(domainID)); + env.close(); + + auto const eurUsdOfferSeq{env.seq(domainOwner)}; + env(offer(domainOwner, eur(20), USD(20)), Domain(domainID)); + env.close(); + + auto const carolBalBefore = env.balance(carol, USD); + + // Both paths compete for the same XRP(10) sendmax. If AMM quality leaks + // into the direct domain book, the engine ranks direct XRP->USD first + // but crossing can only consume the 1:1 LOB offer. With the fix, the + // direct book is ranked by its domain LOB quality, so the 2:1 + // XRP->EUR->USD path executes first. + env(pay(alice, carol, USD(100)), + Path(~USD), + Path(~eur, ~USD), + Sendmax(XRP(10)), + Txflags(tfPartialPayment | tfNoRippleDirect), + Domain(domainID)); + env.close(); + + auto const delivered = env.balance(carol, USD) - carolBalBefore; + if (excludesAmmFromDomainQuality) + { + BEAST_EXPECT(delivered == USD(20)); + + BEAST_EXPECT(checkOffer(env, bob, directOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(!offerExists(env, bob, xrpEurOfferSeq)); + BEAST_EXPECT(!offerExists(env, domainOwner, eurUsdOfferSeq)); + } + else + { + BEAST_EXPECT(delivered == USD(10)); + + BEAST_EXPECT(!offerExists(env, bob, directOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, xrpEurOfferSeq, XRP(10), eur(20), 0, true)); + BEAST_EXPECT(checkOffer(env, domainOwner, eurUsdOfferSeq, eur(20), USD(20), 0, true)); + } + + auto [xrp, usd, lpt] = amm.balances(XRP, USD); + BEAST_EXPECT(xrp == XRP(10)); + BEAST_EXPECT(usd == USD(500)); + } + void testHybridOfferCreate(FeatureBitset features) { @@ -1943,6 +2020,8 @@ public: testOfferTokenIssuerInDomain(all); testRemoveUnfundedOffer(all); testAmmNotUsed(all); + testAmmQualityNotLeaked(all); + testAmmQualityNotLeaked(all - fixCleanup3_3_0); testAutoBridge(all); // Test hybrid offers From bb7c4d1c9fdfd1267ac45198ff305545f9579a72 Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Wed, 24 Jun 2026 08:23:12 -0400 Subject: [PATCH 003/100] fix: Additional RPC validation checks on ammRpcInfo account and amm_account fields. (#7324) --- src/test/jtx/AMM.h | 14 +++++++-- src/test/jtx/impl/AMM.cpp | 30 +++++++++++++++++--- src/test/rpc/AMMInfo_test.cpp | 20 +++++++++++++ src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp | 10 +++++-- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index deadd80290..99131637bb 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -174,12 +174,22 @@ public: ammRpcInfo( std::optional const& account = std::nullopt, std::optional const& ledgerIndex = std::nullopt, - std::optional asset1 = std::nullopt, - std::optional asset2 = std::nullopt, + std::optional const& asset1 = std::nullopt, + std::optional const& asset2 = std::nullopt, std::optional const& ammAccount = std::nullopt, bool ignoreParams = false, unsigned apiVersion = RPC::kApiInvalidVersion) const; + [[nodiscard]] json::Value + ammRpcInfo( + std::optional const& account, + std::optional const& ledgerIndex, + std::optional const& asset1, + std::optional const& asset2, + std::optional const& ammAccount, + bool ignoreParams, + unsigned apiVersion) const; + /** Verify the AMM balances. */ [[nodiscard]] bool diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index c6dc14081a..2184f7e1b0 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -183,15 +183,37 @@ json::Value AMM::ammRpcInfo( std::optional const& account, std::optional const& ledgerIndex, - std::optional asset1, - std::optional asset2, + std::optional const& asset1, + std::optional const& asset2, std::optional const& ammAccount, bool ignoreParams, unsigned apiVersion) const +{ + auto const toJson = [](AccountID const& a) { return json::Value{to_string(a)}; }; + + return ammRpcInfo( + account.transform(toJson), + ledgerIndex, + asset1, + asset2, + ammAccount.transform(toJson), + ignoreParams, + apiVersion); +} + +json::Value +AMM::ammRpcInfo( + std::optional const& account, + std::optional const& ledgerIndex, + std::optional const& asset1, + std::optional const& asset2, + std::optional const& ammAccount, + bool ignoreParams, + unsigned apiVersion) const { json::Value jv; if (account) - jv[jss::account] = to_string(*account); + jv[jss::account] = *account; if (ledgerIndex) jv[jss::ledger_index] = *ledgerIndex; if (!ignoreParams) @@ -209,7 +231,7 @@ AMM::ammRpcInfo( jv[jss::asset2] = STIssue(sfAsset2, asset2_.asset()).getJson(JsonOptions::Values::None); } if (ammAccount) - jv[jss::amm_account] = to_string(*ammAccount); + jv[jss::amm_account] = *ammAccount; } auto jr = (apiVersion == RPC::kApiInvalidVersion diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index 28c536aab9..987df6c724 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -65,6 +65,26 @@ public: BEAST_EXPECT(jv[jss::error_message] == "Account malformed."); }); + // Account is not a string + testAMM([&](AMM& ammAlice, Env&) { + auto const jv = + ammAlice.ammRpcInfo(json::Value{42}, std::nullopt, XRP, USD, std::nullopt, true, 3); + BEAST_EXPECT(jv[jss::error_message] == "Account malformed."); + }); + + // AMM Account is not a string + testAMM([&](AMM& ammAlice, Env&) { + auto const jv = ammAlice.ammRpcInfo( + json::Value{to_string(ammAlice.ammAccount())}, + std::nullopt, + XRP, + USD, + json::Value{42}, + false, + 3); + BEAST_EXPECT(jv[jss::error_message] == "Account malformed."); + }); + std::vector, std::optional, TestAccount, bool>> const invalidParams = { {xrpIssue(), std::nullopt, TestAccount::None, false}, diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp index 2c2f96b0e8..043fdd2d7b 100644 --- a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp @@ -120,7 +120,10 @@ doAMMInfo(RPC::JsonContext& context) if (params.isMember(jss::amm_account)) { - auto const id = parseBase58((params[jss::amm_account].asString())); + auto const& ammAccount = params[jss::amm_account]; + if (!ammAccount.isString()) + return std::unexpected(RpcActMalformed); + auto const id = parseBase58(ammAccount.asString()); if (!id) return std::unexpected(RpcActMalformed); auto const sle = ledger->read(keylet::account(*id)); @@ -133,7 +136,10 @@ doAMMInfo(RPC::JsonContext& context) if (params.isMember(jss::account)) { - accountID = parseBase58(params[jss::account].asString()); + auto const& localAccount = params[jss::account]; + if (!localAccount.isString()) + return std::unexpected(RpcActMalformed); + accountID = parseBase58(localAccount.asString()); if (!accountID || !ledger->read(keylet::account(*accountID))) return std::unexpected(RpcActMalformed); } From b68e1f7170fd0de7b7b7110677919eb9df1773c2 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 24 Jun 2026 13:24:04 +0100 Subject: [PATCH 004/100] fix: Add pragma once checker (#7580) --- .pre-commit-config.yaml | 8 ++++- bin/pre-commit/fix_pragma_once.py | 34 +++++++++++++++++++ .../ledger/helpers/PermissionedDEXHelpers.h | 1 + include/xrpl/protocol/Batch.h | 2 ++ include/xrpl/tx/paths/detail/AmountSpec.h | 0 include/xrpl/tx/paths/detail/FlowDebugInfo.h | 1 - include/xrpl/tx/paths/detail/StrandFlow.h | 1 - src/libxrpl/tx/paths/Flow.cpp | 1 - src/libxrpl/tx/paths/XRPEndpointStep.cpp | 1 - src/libxrpl/tx/transactors/escrow/Escrow.cpp | 0 src/test/beast/IPEndpointCommon.h | 2 ++ src/test/csf.h | 2 ++ src/test/unit_test/utils.h | 2 ++ src/xrpld/app/ledger/OrderBookDB.h | 0 src/xrpld/overlay/detail/Tuning.h | 1 + src/xrpld/rpc/detail/PathRequestManager.h | 1 - src/xrpld/rpc/detail/Pathfinder.cpp | 1 - src/xrpld/rpc/detail/RippleLineCache.cpp | 0 src/xrpld/rpc/detail/RippleLineCache.h | 0 .../rpc/handlers/ledger/LedgerEntryHelpers.h | 2 ++ 20 files changed, 53 insertions(+), 7 deletions(-) create mode 100755 bin/pre-commit/fix_pragma_once.py delete mode 100644 include/xrpl/tx/paths/detail/AmountSpec.h delete mode 100644 src/libxrpl/tx/transactors/escrow/Escrow.cpp delete mode 100644 src/xrpld/app/ledger/OrderBookDB.h delete mode 100644 src/xrpld/rpc/detail/RippleLineCache.cpp delete mode 100644 src/xrpld/rpc/detail/RippleLineCache.h diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9dec89435..4cbf4c1dd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,7 @@ repos: hooks: - id: check-added-large-files args: [--maxkb=400, --enforce-all] + - id: check-executables-have-shebangs - id: trailing-whitespace - id: end-of-file-fixer - id: check-merge-conflict @@ -35,13 +36,18 @@ repos: language: python types_or: [c++, c] exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/ + - id: fix-pragma-once + name: fix missing '#pragma once' declarations in header files + language: python + entry: ./bin/pre-commit/fix_pragma_once.py + files: \.(h|hpp)$ - repo: https://github.com/pre-commit/mirrors-clang-format rev: dd18dad857d6133e90bbe478f4f2f22ec0030269 # frozen: v22.1.5 hooks: - id: clang-format args: [--style=file] - "types_or": [c++, c, proto] + types_or: [c++, c, proto] exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/ - repo: https://github.com/BlankSpruce/gersemi-pre-commit diff --git a/bin/pre-commit/fix_pragma_once.py b/bin/pre-commit/fix_pragma_once.py new file mode 100755 index 0000000000..08a505b6d0 --- /dev/null +++ b/bin/pre-commit/fix_pragma_once.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +""" +Adds "#pragma once" to the top of header files that don't already have it. + +Usage: ./bin/pre-commit/fix_pragma_once.py ... +""" + +import sys +from pathlib import Path + +PRAGMA_ONCE = "#pragma once\n\n" + + +def fix_pragma_once(path: Path) -> bool: + original = path.read_text(encoding="utf-8") + if PRAGMA_ONCE not in original: + path.write_text(PRAGMA_ONCE + original, encoding="utf-8") + return False + return True + + +def main() -> int: + files = [Path(f) for f in sys.argv[1:]] + success = True + + for path in files: + success &= fix_pragma_once(path) + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h index 04b12f2fc5..695a4950f0 100644 --- a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h +++ b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h @@ -1,4 +1,5 @@ #pragma once + #include namespace xrpl::permissioned_dex { diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h index fa7641af70..2f2412b3ff 100644 --- a/include/xrpl/protocol/Batch.h +++ b/include/xrpl/protocol/Batch.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include diff --git a/include/xrpl/tx/paths/detail/AmountSpec.h b/include/xrpl/tx/paths/detail/AmountSpec.h deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/include/xrpl/tx/paths/detail/FlowDebugInfo.h b/include/xrpl/tx/paths/detail/FlowDebugInfo.h index ec7df86e53..1ccfba34ce 100644 --- a/include/xrpl/tx/paths/detail/FlowDebugInfo.h +++ b/include/xrpl/tx/paths/detail/FlowDebugInfo.h @@ -3,7 +3,6 @@ #include #include #include -#include #include diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index 31f0182258..4d988a4c7e 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include diff --git a/src/libxrpl/tx/paths/Flow.cpp b/src/libxrpl/tx/paths/Flow.cpp index 39a9e83e69..7be1f9f633 100644 --- a/src/libxrpl/tx/paths/Flow.cpp +++ b/src/libxrpl/tx/paths/Flow.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include diff --git a/src/libxrpl/tx/paths/XRPEndpointStep.cpp b/src/libxrpl/tx/paths/XRPEndpointStep.cpp index 314780c3a7..efdad92791 100644 --- a/src/libxrpl/tx/paths/XRPEndpointStep.cpp +++ b/src/libxrpl/tx/paths/XRPEndpointStep.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include diff --git a/src/libxrpl/tx/transactors/escrow/Escrow.cpp b/src/libxrpl/tx/transactors/escrow/Escrow.cpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/test/beast/IPEndpointCommon.h b/src/test/beast/IPEndpointCommon.h index 15566cb830..0ff0da35be 100644 --- a/src/test/beast/IPEndpointCommon.h +++ b/src/test/beast/IPEndpointCommon.h @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/src/test/csf.h b/src/test/csf.h index 81af3491c4..d2ddbb460d 100644 --- a/src/test/csf.h +++ b/src/test/csf.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include diff --git a/src/test/unit_test/utils.h b/src/test/unit_test/utils.h index 028823c763..677bbff31b 100644 --- a/src/test/unit_test/utils.h +++ b/src/test/unit_test/utils.h @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/src/xrpld/app/ledger/OrderBookDB.h b/src/xrpld/app/ledger/OrderBookDB.h deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index 20a60d470e..8357fcd130 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -1,4 +1,5 @@ #pragma once + #include #include diff --git a/src/xrpld/rpc/detail/PathRequestManager.h b/src/xrpld/rpc/detail/PathRequestManager.h index 5a5cfde402..c8e272a97d 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.h +++ b/src/xrpld/rpc/detail/PathRequestManager.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index 25da86ef8f..e1a2a4acf6 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/src/xrpld/rpc/detail/RippleLineCache.cpp b/src/xrpld/rpc/detail/RippleLineCache.cpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/xrpld/rpc/detail/RippleLineCache.h b/src/xrpld/rpc/detail/RippleLineCache.h deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h index 463547a90d..11f6553dfa 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h @@ -1,3 +1,5 @@ +#pragma once + #include #include From 6736ab39df871bbb49107c0af8a2821fc3afebaf Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 24 Jun 2026 08:24:27 -0400 Subject: [PATCH 005/100] test: Add test for Permissioned Domain sequence fix (#7591) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/test/app/PermissionedDomains_test.cpp | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/app/PermissionedDomains_test.cpp b/src/test/app/PermissionedDomains_test.cpp index f2d7bce152..2cffc18682 100644 --- a/src/test/app/PermissionedDomains_test.cpp +++ b/src/test/app/PermissionedDomains_test.cpp @@ -8,18 +8,21 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -526,6 +529,47 @@ class PermissionedDomains_test : public beast::unit_test::Suite BEAST_EXPECT(env.ownerCount(alice) == 1); } + void + testTicket(FeatureBitset features) + { + testcase("Tickets"); + + using namespace test::jtx; + + Env env(*this, features); + Account const alice("alice"); + env.fund(XRP(1000), alice); + + pdomain::Credentials const credentials{ + {.issuer = alice, .credType = "credential1"}, + }; + + std::uint32_t seq{env.seq(alice)}; + env(ticket::create(alice, 2)); + + { + env(pdomain::setTx(alice, credentials), ticket::Use(++seq)); + auto domain = pdomain::getNewDomain(env.meta()); + if (features[fixCleanup3_1_3]) + { + BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), seq).key); + } + else + { + BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), 0).key); + } + } + + if (features[fixCleanup3_1_3]) + { + env(pdomain::setTx(alice, credentials), ticket::Use(++seq)); + } + else + { + env(pdomain::setTx(alice, credentials), ticket::Use(++seq), Ter(tefEXCEPTION)); + } + } + public: void run() override @@ -540,6 +584,8 @@ public: testDelete(withFix_); testAccountReserve(withFeature_); testAccountReserve(withFix_); + testTicket(withFeature_); + testTicket(withFix_); } }; From 8bbbc2051e04edb4d0959deb6c8b322e3e29a63e Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 24 Jun 2026 13:25:03 +0100 Subject: [PATCH 006/100] chore: Check more tools to be available (#7600) --- .github/scripts/strategy-matrix/linux.json | 2 +- .github/workflows/on-pr.yml | 1 + .github/workflows/on-trigger.yml | 1 + .github/workflows/publish-docs.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/reusable-upload-recipe.yml | 2 +- bin/check-tools.sh | 3 +++ 7 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index a9b85b766a..863b910dda 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -1,5 +1,5 @@ { - "image_tag": "sha-fe4c8ae", + "image_tag": "sha-e29b523", "configs": { "ubuntu": [ { diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 0cc9b375a7..0c9eeda712 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -70,6 +70,7 @@ jobs: .github/workflows/reusable-upload-recipe.yml .clang-tidy .codecov.yml + bin/check-tools.sh cfg/** cmake/** conan/** diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 74bca82019..063cdbff7f 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -27,6 +27,7 @@ on: - ".github/workflows/reusable-upload-recipe.yml" - ".clang-tidy" - ".codecov.yml" + - "bin/check-tools.sh" - "cfg/**" - "cmake/**" - "conan/**" diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index cc7b6b6e7e..cb7d4c5382 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -41,7 +41,7 @@ env: jobs: build: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index e99ef574bf..f36463a5d0 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -39,7 +39,7 @@ jobs: needs: [determine-files] if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }} runs-on: ["self-hosted", "Linux", "X64", "heavy"] - container: "ghcr.io/xrplf/xrpld/nix-debian:sha-fe4c8ae" + container: "ghcr.io/xrplf/xrpld/nix-debian:sha-e29b523" permissions: contents: read issues: write diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index a389e98771..a18f76796a 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -40,7 +40,7 @@ defaults: jobs: upload: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/bin/check-tools.sh b/bin/check-tools.sh index 15b16b6fc8..808f384d5b 100755 --- a/bin/check-tools.sh +++ b/bin/check-tools.sh @@ -90,16 +90,19 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then check perl check pkg-config check vim + check zip # These tools are present in our Linux CI images and in local development # setups, but not in the macOS CI environment. So check them everywhere # except when running in CI on macOS. if [ "${os}" = "linux" ] || [ -z "${CI:-}" ]; then check clang-format + check dot check doxygen check gcovr check gh check git-cliff + check git-lfs check gpg # pre-commit, or its alternative implementation prek check pre-commit sh -c 'pre-commit --version || prek --version' From 4fec58251b8d20fd5356f08ed04ddc80d008c2bf Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 24 Jun 2026 14:56:18 +0100 Subject: [PATCH 007/100] build: Patch nix binaries in CMake (#7539) Co-authored-by: Bart --- .gersemi/definitions.cmake | 3 ++ .../workflows/reusable-build-test-config.yml | 15 ------ CMakeLists.txt | 2 + cmake/CompilationEnv.cmake | 13 +++++ cmake/PatchNixBinary.cmake | 53 +++++++++++++++++++ cmake/XrplCompiler.cmake | 9 ++++ cmake/XrplCore.cmake | 1 + cmake/XrplSanitizers.cmake | 4 +- src/tests/libxrpl/CMakeLists.txt | 1 + 9 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 cmake/PatchNixBinary.cmake diff --git a/.gersemi/definitions.cmake b/.gersemi/definitions.cmake index 245f827f90..58bc74c70a 100644 --- a/.gersemi/definitions.cmake +++ b/.gersemi/definitions.cmake @@ -96,3 +96,6 @@ function(verbose_find_path variable name) ${ARGN} ) endfunction() + +function(patch_nix_binary target) +endfunction() diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index fe441dba6e..a81d9aec67 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -229,21 +229,6 @@ jobs: --parallel "${BUILD_NPROC}" \ --target "${CMAKE_TARGET}" - # This step is needed to allow running in non-Nix environments - - name: Patch binary to use default loader and remove rpath (Linux) - if: ${{ runner.os == 'Linux' && env.SANITIZERS_ENABLED == 'false' }} - run: | - loader="$(/tmp/loader-path.sh)" - patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/xrpld" - - # We're only running aarch64 Linux builds in Ubuntu-based images, so this is kept simple - - name: Install libatomic (Linux aarch64) - if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' }} - run: | - apt update --yes - apt install -y --no-install-recommends \ - libatomic1 - - name: Show ccache statistics if: ${{ inputs.ccache_enabled }} run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dbe60a220..bdc62442b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,8 @@ if(target) ) endif() +include(PatchNixBinary) + include(XrplSanity) include(XrplVersion) include(XrplSettings) diff --git a/cmake/CompilationEnv.cmake b/cmake/CompilationEnv.cmake index 0d44f90974..8e69a4dfdd 100644 --- a/cmake/CompilationEnv.cmake +++ b/cmake/CompilationEnv.cmake @@ -56,3 +56,16 @@ elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") else() message(FATAL_ERROR "Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}") endif() + +# -------------------------------------------------------------------- +# Sanitizers +# -------------------------------------------------------------------- +# SANITIZERS is injected by the Conan toolchain when a sanitizer build is +# requested (see conan/profiles/sanitizers). The flags are applied to the +# 'common' target in XrplSanitizers; this flag lets other modules know a +# sanitizer build is active without depending on that module. +if(DEFINED SANITIZERS) + set(SANITIZERS_ENABLED TRUE) +else() + set(SANITIZERS_ENABLED FALSE) +endif() diff --git a/cmake/PatchNixBinary.cmake b/cmake/PatchNixBinary.cmake new file mode 100644 index 0000000000..79ca0b150c --- /dev/null +++ b/cmake/PatchNixBinary.cmake @@ -0,0 +1,53 @@ +#[===================================================================[ + Patch executables to run in non-Nix environments. + + The Nix-based CI image links binaries against an ELF interpreter (loader) + that lives in the Nix store, so the resulting binaries don't run elsewhere + (including once installed from the .deb package). `patch_nix_binary` adds a + POST_BUILD step that resets the interpreter to the system default loader and + drops the rpath. + + This is only active inside the Nix-based image, detected by the presence of + /tmp/loader-path.sh (shipped by that image, resolves the default loader). It + is skipped for sanitizer builds, whose runtime libraries are resolved through + the rpath. Everywhere else `patch_nix_binary` is a no-op. +#]===================================================================] + +include_guard(GLOBAL) + +include(CompilationEnv) + +# Provided by the Nix-based CI image; prints the system default ELF loader path. +set(_loader_path_script "/tmp/loader-path.sh") + +if(is_linux AND NOT SANITIZERS_ENABLED AND EXISTS "${_loader_path_script}") + execute_process( + COMMAND "${_loader_path_script}" + OUTPUT_VARIABLE DEFAULT_LOADER_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) + find_program(PATCHELF_COMMAND patchelf REQUIRED) + set(PATCH_NIX_BINARIES TRUE) + message( + STATUS + "Binaries will be patched to use loader '${DEFAULT_LOADER_PATH}'" + ) +else() + set(PATCH_NIX_BINARIES FALSE) +endif() + +function(patch_nix_binary target) + if(NOT PATCH_NIX_BINARIES) + return() + endif() + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND + "${PATCHELF_COMMAND}" --set-interpreter "${DEFAULT_LOADER_PATH}" + --remove-rpath "$" + COMMENT "Patching ${target}: set default loader, remove rpath" + VERBATIM + ) +endfunction() diff --git a/cmake/XrplCompiler.cmake b/cmake/XrplCompiler.cmake index 9af8e962d0..cb4e797137 100644 --- a/cmake/XrplCompiler.cmake +++ b/cmake/XrplCompiler.cmake @@ -154,6 +154,15 @@ else() > ) + # On aarch64, libatomic is required for atomic operations. It is not needed on x86_64. + # Linking it statically on Linux + if(is_arm64 AND is_linux) + target_link_options( + common + INTERFACE -Wl,--push-state -Wl,-Bstatic -latomic -Wl,--pop-state + ) + endif() + # Keep -stdlib=libstdc++ off the compile commands, but preserve it for linking. # # Conan turns `compiler.libcxx=libstdc++` into `-stdlib=libstdc++` and puts it in diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index 52d7714a99..4d4a800d9a 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -247,6 +247,7 @@ target_link_modules( if(xrpld) add_executable(xrpld) + patch_nix_binary(xrpld) if(tests) target_compile_definitions(xrpld PUBLIC ENABLE_TESTS) target_compile_definitions( diff --git a/cmake/XrplSanitizers.cmake b/cmake/XrplSanitizers.cmake index 64f1841bfb..893c880374 100644 --- a/cmake/XrplSanitizers.cmake +++ b/cmake/XrplSanitizers.cmake @@ -14,11 +14,9 @@ include_guard(GLOBAL) include(CompilationEnv) -if(NOT DEFINED SANITIZERS) - set(SANITIZERS_ENABLED FALSE) +if(NOT SANITIZERS_ENABLED) return() endif() -set(SANITIZERS_ENABLED TRUE) message(STATUS "=== Configuring Sanitizers ===") message(STATUS " SANITIZERS: ${SANITIZERS}") diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 2dae6fccb9..bd56028728 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable( helpers/TestSink.cpp helpers/TxTest.cpp ) +patch_nix_binary(xrpl_tests) set_target_properties( xrpl_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" From eef8f4a4ff29a76acdc261ab2a309b55ae3cb18e Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 24 Jun 2026 18:23:29 +0100 Subject: [PATCH 008/100] chore: Use clang-tidy v22 new features (#7427) --- .clang-tidy | 8 ++- .../scripts/levelization/results/ordering.txt | 1 - include/xrpl/basics/DecayingSample.h | 6 +- include/xrpl/basics/Log.h | 3 +- include/xrpl/basics/TaggedCache.h | 4 +- include/xrpl/basics/TaggedCache.ipp | 4 +- include/xrpl/basics/hardened_hash.h | 2 +- .../xrpl/basics/partitioned_unordered_map.h | 8 +-- include/xrpl/basics/random.h | 1 + include/xrpl/beast/asio/io_latency_probe.h | 4 +- include/xrpl/beast/clock/abstract_clock.h | 8 +-- .../xrpl/beast/clock/basic_seconds_clock.h | 8 +-- .../detail/aged_container_iterator.h | 6 +- .../container/detail/aged_ordered_container.h | 29 ++++---- .../detail/aged_unordered_container.h | 32 ++++----- include/xrpl/beast/core/List.h | 6 +- include/xrpl/beast/core/LockFreeStack.h | 6 +- include/xrpl/beast/hash/uhash.h | 2 +- include/xrpl/beast/rfc2616.h | 2 +- .../beast/unit_test/detail/const_container.h | 10 +-- include/xrpl/beast/unit_test/reporter.h | 14 ++-- include/xrpl/beast/utility/Journal.h | 6 +- include/xrpl/beast/utility/maybe_const.h | 2 +- include/xrpl/beast/utility/rngfill.h | 4 +- include/xrpl/json/json_value.h | 7 +- include/xrpl/protocol/KnownFormats.h | 4 +- include/xrpl/protocol/STBitString.h | 2 +- include/xrpl/protocol/STInteger.h | 2 +- include/xrpl/protocol/STObject.h | 25 +++---- include/xrpl/protocol/TER.h | 4 +- include/xrpl/protocol/Units.h | 2 +- include/xrpl/protocol/XChainAttestations.h | 10 +-- include/xrpl/protocol/digest.h | 4 +- include/xrpl/shamap/FullBelowCache.h | 2 +- src/libxrpl/json/json_valueiterator.cpp | 2 +- src/libxrpl/protocol/XChainAttestations.cpp | 8 +-- src/libxrpl/protocol/tokens.cpp | 8 +-- src/libxrpl/shamap/SHAMapInnerNode.cpp | 2 +- src/libxrpl/tx/invariants/InvariantCheck.cpp | 2 + src/libxrpl/tx/paths/BookStep.cpp | 4 +- src/libxrpl/tx/paths/Flow.cpp | 4 +- .../tx/transactors/system/TicketCreate.cpp | 2 +- src/test/app/LedgerReplay_test.cpp | 4 +- src/test/app/Loan_test.cpp | 4 +- src/test/app/NFTokenDir_test.cpp | 10 +-- src/test/app/OfferMPT_test.cpp | 4 +- src/test/app/Offer_test.cpp | 8 +-- src/test/app/TxQ_test.cpp | 2 +- .../beast/aged_associative_container_test.cpp | 68 +++++++++---------- .../beast/beast_io_latency_probe_test.cpp | 4 +- .../consensus/ByzantineFailureSim_test.cpp | 1 - src/test/consensus/Consensus_test.cpp | 1 - .../DistributedValidatorsSim_test.cpp | 1 - src/test/consensus/ScaleFreeSim_test.cpp | 1 - src/test/csf/BasicNetwork.h | 4 +- src/test/csf/Digraph.h | 6 +- src/test/csf/Scheduler.h | 18 ++--- src/test/csf/SimTime.h | 4 +- src/test/csf/random.h | 4 +- src/test/jtx/TestHelpers.h | 7 +- src/test/nodestore/Timing_test.cpp | 2 +- src/test/server/Server_test.cpp | 2 +- src/test/unit_test/multi_runner.cpp | 4 +- src/test/unit_test/multi_runner.h | 6 +- src/xrpld/app/consensus/RCLConsensus.cpp | 4 ++ src/xrpld/app/consensus/RCLValidations.cpp | 2 + src/xrpld/app/ledger/LedgerHistory.cpp | 4 ++ src/xrpld/app/ledger/detail/BuildLedger.cpp | 2 + src/xrpld/app/ledger/detail/LedgerCleaner.cpp | 2 + src/xrpld/app/ledger/detail/LedgerMaster.cpp | 6 ++ src/xrpld/app/main/Application.cpp | 2 + src/xrpld/app/misc/NetworkOPs.cpp | 2 + src/xrpld/app/misc/detail/Transaction.cpp | 2 +- src/xrpld/app/misc/detail/ValidatorSite.cpp | 2 + src/xrpld/app/rdb/backend/detail/Node.cpp | 10 +-- src/xrpld/consensus/Consensus.h | 30 ++++---- src/xrpld/consensus/ConsensusTypes.h | 8 +-- src/xrpld/consensus/DisputedTx.h | 2 +- src/xrpld/consensus/LedgerTrie.h | 12 ++-- src/xrpld/consensus/Validations.h | 14 ++-- src/xrpld/overlay/Slot.h | 21 +++--- src/xrpld/overlay/Squelch.h | 2 +- src/xrpld/overlay/detail/PeerImp.h | 2 +- src/xrpld/overlay/detail/ZeroCopyStream.h | 6 +- src/xrpld/peerfinder/detail/Checker.h | 8 +-- src/xrpld/peerfinder/detail/Livecache.h | 23 +++---- src/xrpld/peerfinder/detail/Logic.h | 2 +- src/xrpld/rpc/detail/RPCCall.cpp | 2 +- .../handlers/admin/keygen/WalletPropose.cpp | 2 +- src/xrpld/rpc/json_body.h | 2 +- 90 files changed, 315 insertions(+), 294 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index e8e2ca7ac9..35427810a3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -11,6 +11,7 @@ Checks: "-*, bugprone-copy-constructor-init, bugprone-crtp-constructor-accessibility, bugprone-dangling-handle, + bugprone-derived-method-shadowing-base-method, bugprone-dynamic-static-initializers, bugprone-empty-catch, bugprone-fold-init-type, @@ -21,6 +22,7 @@ Checks: "-*, bugprone-incorrect-roundings, bugprone-infinite-loop, bugprone-integer-division, + bugprone-invalid-enum-default-initialization, bugprone-lambda-function-name, bugprone-macro-parentheses, bugprone-macro-repeated-side-effects, @@ -137,6 +139,7 @@ Checks: "-*, readability-enum-initial-value, readability-identifier-naming, readability-implicit-bool-conversion, + readability-inconsistent-ifelse-braces, readability-make-member-function-const, readability-math-missing-parentheses, readability-misleading-indentation, @@ -145,7 +148,9 @@ Checks: "-*, readability-redundant-declaration, readability-redundant-inline-specifier, readability-redundant-member-init, + readability-redundant-parentheses, readability-redundant-string-init, + readability-redundant-typename, readability-reference-to-constructed-temporary, readability-simplify-boolean-expr, readability-static-definition-in-anonymous-namespace, @@ -153,7 +158,8 @@ Checks: "-*, readability-use-std-min-max " # --- -# bugprone-narrowing-conversions, # this will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs +# bugprone-narrowing-conversions, # This will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs +# misc-override-with-different-visibility, # Will be addressed in a future PR, but for now it generates too many warnings # readability-inconsistent-declaration-parameter-name, # In this codebase this check will break a lot of arg names # readability-static-accessed-through-instance, # this check is probably unnecessary. It makes the code less readable # --- diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 547c1b3539..7b31042158 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -83,7 +83,6 @@ test.conditions > xrpl.basics test.conditions > xrpl.conditions test.consensus > test.csf test.consensus > test.jtx -test.consensus > test.toplevel test.consensus > test.unit_test test.consensus > xrpl.basics test.consensus > xrpld.app diff --git a/include/xrpl/basics/DecayingSample.h b/include/xrpl/basics/DecayingSample.h index d1861ebc4a..910c8f9e14 100644 --- a/include/xrpl/basics/DecayingSample.h +++ b/include/xrpl/basics/DecayingSample.h @@ -12,8 +12,8 @@ template class DecayingSample { public: - using value_type = typename Clock::duration::rep; - using time_point = typename Clock::time_point; + using value_type = Clock::duration::rep; + using time_point = Clock::time_point; DecayingSample() = delete; @@ -93,7 +93,7 @@ template class DecayWindow { public: - using time_point = typename Clock::time_point; + using time_point = Clock::time_point; explicit DecayWindow(time_point now) : when_(now) { diff --git a/include/xrpl/basics/Log.h b/include/xrpl/basics/Log.h index 6bafbc7c54..0699cdd3d9 100644 --- a/include/xrpl/basics/Log.h +++ b/include/xrpl/basics/Log.h @@ -206,8 +206,7 @@ private: #ifndef JLOG #define JLOG(x) \ if (!(x)) \ - { \ - } \ + ; \ else \ x #endif diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h index ecf6071f8d..973fcd828a 100644 --- a/include/xrpl/basics/TaggedCache.h +++ b/include/xrpl/basics/TaggedCache.h @@ -338,7 +338,7 @@ private: sweepHelper( clock_type::time_point const& whenExpire, [[maybe_unused]] clock_type::time_point const& now, - typename KeyValueCacheType::map_type& partition, + KeyValueCacheType::map_type& partition, SweptPointersVector& stuffToSweep, std::atomic& allRemovals, std::scoped_lock const&); @@ -347,7 +347,7 @@ private: sweepHelper( clock_type::time_point const& whenExpire, clock_type::time_point const& now, - typename KeyOnlyCacheType::map_type& partition, + KeyOnlyCacheType::map_type& partition, SweptPointersVector&, std::atomic& allRemovals, std::scoped_lock const&); diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index 6973ec4ba0..7e812ce4c7 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -735,7 +735,7 @@ TaggedCache& allRemovals, std::scoped_lock const&) @@ -815,7 +815,7 @@ TaggedCache& allRemovals, std::scoped_lock const&) diff --git a/include/xrpl/basics/hardened_hash.h b/include/xrpl/basics/hardened_hash.h index efc77e058b..b8ea1e0f3f 100644 --- a/include/xrpl/basics/hardened_hash.h +++ b/include/xrpl/basics/hardened_hash.h @@ -75,7 +75,7 @@ private: detail::seed_pair seeds_{detail::makeSeedPair<>()}; public: - using result_type = typename HashAlgorithm::result_type; + using result_type = HashAlgorithm::result_type; HardenedHash() = default; diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h index 3bf64985e5..c51cedf2dd 100644 --- a/include/xrpl/basics/partitioned_unordered_map.h +++ b/include/xrpl/basics/partitioned_unordered_map.h @@ -57,8 +57,8 @@ public: { using iterator_category = std::forward_iterator_tag; partition_map_type* map{nullptr}; - typename partition_map_type::iterator ait{}; - typename map_type::iterator mit; + partition_map_type::iterator ait{}; + map_type::iterator mit; Iterator() = default; @@ -126,8 +126,8 @@ public: using iterator_category = std::forward_iterator_tag; partition_map_type* map{nullptr}; - typename partition_map_type::iterator ait{}; - typename map_type::iterator mit; + partition_map_type::iterator ait{}; + map_type::iterator mit; ConstIterator() = default; diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index 17f4a1c213..0b298e12d9 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -29,6 +29,7 @@ static_assert( namespace detail { // Determines if a type can be called like an Engine +// NOLINTNEXTLINE(readability-redundant-typename): typename required by MSVC template using is_engine = std::is_invocable_r; } // namespace detail diff --git a/include/xrpl/beast/asio/io_latency_probe.h b/include/xrpl/beast/asio/io_latency_probe.h index ce3929a394..5e1b098dcb 100644 --- a/include/xrpl/beast/asio/io_latency_probe.h +++ b/include/xrpl/beast/asio/io_latency_probe.h @@ -18,8 +18,8 @@ template class IOLatencyProbe { private: - using duration = typename Clock::duration; - using time_point = typename Clock::time_point; + using duration = Clock::duration; + using time_point = Clock::time_point; std::recursive_mutex mutex_; std::condition_variable_any cond_; diff --git a/include/xrpl/beast/clock/abstract_clock.h b/include/xrpl/beast/clock/abstract_clock.h index 33d4096d0e..15d785d138 100644 --- a/include/xrpl/beast/clock/abstract_clock.h +++ b/include/xrpl/beast/clock/abstract_clock.h @@ -34,10 +34,10 @@ template class AbstractClock { public: - using rep = typename Clock::rep; - using period = typename Clock::period; - using duration = typename Clock::duration; - using time_point = typename Clock::time_point; + using rep = Clock::rep; + using period = Clock::period; + using duration = Clock::duration; + using time_point = Clock::time_point; using clock_type = Clock; static bool const is_steady = Clock::is_steady; // NOLINT(readability-identifier-naming) diff --git a/include/xrpl/beast/clock/basic_seconds_clock.h b/include/xrpl/beast/clock/basic_seconds_clock.h index 8460dbe26b..5a267e9458 100644 --- a/include/xrpl/beast/clock/basic_seconds_clock.h +++ b/include/xrpl/beast/clock/basic_seconds_clock.h @@ -20,10 +20,10 @@ public: explicit BasicSecondsClock() = default; - using rep = typename Clock::rep; - using period = typename Clock::period; - using duration = typename Clock::duration; - using time_point = typename Clock::time_point; + using rep = Clock::rep; + using period = Clock::period; + using duration = Clock::duration; + using time_point = Clock::time_point; static bool const is_steady = // NOLINT(readability-identifier-naming) Clock::is_steady; diff --git a/include/xrpl/beast/container/detail/aged_container_iterator.h b/include/xrpl/beast/container/detail/aged_container_iterator.h index 8d44646cd4..02fb3927dd 100644 --- a/include/xrpl/beast/container/detail/aged_container_iterator.h +++ b/include/xrpl/beast/container/detail/aged_container_iterator.h @@ -16,15 +16,15 @@ template class AgedContainerIterator { public: - using iterator_category = typename std::iterator_traits::iterator_category; + using iterator_category = std::iterator_traits::iterator_category; using value_type = std::conditional_t< IsConst, typename Iterator::value_type::Stashed::value_type const, typename Iterator::value_type::Stashed::value_type>; - using difference_type = typename std::iterator_traits::difference_type; + using difference_type = std::iterator_traits::difference_type; using pointer = value_type*; using reference = value_type&; - using time_point = typename Iterator::value_type::Stashed::time_point; + using time_point = Iterator::value_type::Stashed::time_point; AgedContainerIterator() = default; diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index ad368f2754..4cb2246a22 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -62,8 +62,8 @@ class AgedOrderedContainer { public: using clock_type = AbstractClock; - using time_point = typename clock_type::time_point; - using duration = typename clock_type::duration; + using time_point = clock_type::time_point; + using duration = clock_type::duration; using key_type = Key; using mapped_type = T; using value_type = std::conditional_t, Key>; @@ -94,8 +94,8 @@ private: { explicit Stashed() = default; - using value_type = typename AgedOrderedContainer::value_type; - using time_point = typename AgedOrderedContainer::time_point; + using value_type = AgedOrderedContainer::value_type; + using time_point = AgedOrderedContainer::time_point; }; Element(time_point const& when, value_type const& value) : value(value), when(when) @@ -192,8 +192,8 @@ private: } }; - using list_type = typename boost::intrusive:: - make_list>::type; + using list_type = + boost::intrusive::make_list>::type; using cont_type = std::conditional_t< IsMulti, @@ -206,8 +206,7 @@ private: boost::intrusive::constant_time_size, boost::intrusive::compare>::type>; - using ElementAllocator = - typename std::allocator_traits::template rebind_alloc; + using ElementAllocator = std::allocator_traits::template rebind_alloc; using ElementAllocatorTraits = std::allocator_traits; @@ -373,8 +372,8 @@ public: using allocator_type = Allocator; using reference = value_type&; using const_reference = value_type const&; - using pointer = typename std::allocator_traits::pointer; - using const_pointer = typename std::allocator_traits::const_pointer; + using pointer = std::allocator_traits::pointer; + using const_pointer = std::allocator_traits::const_pointer; // A set iterator (IsMap==false) is always const // because the elements of a set are immutable. @@ -617,7 +616,7 @@ public: bool MaybeMulti = IsMulti, bool MaybeMap = IsMap, class = std::enable_if_t> - typename std::conditional::type const& + std::conditional::type const& at(K const& k) const; template < @@ -1146,7 +1145,7 @@ private: void touch( beast::detail::AgedContainerIterator pos, - typename clock_type::time_point const& now); + clock_type::time_point const& now); template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> @@ -1393,7 +1392,7 @@ AgedOrderedContainer::at(K co template template -typename std::conditional::type const& +std::conditional::type const& AgedOrderedContainer::at(K const& k) const { auto const iter(cont_.find(k, std::cref(config_.keyCompare()))); @@ -1732,7 +1731,7 @@ AgedOrderedContainer::operato cend(), other.cbegin(), other.cend(), - [&eq, &other](value_type const& lhs, typename Other::value_type const& rhs) { + [&eq, &other](value_type const& lhs, Other::value_type const& rhs) { return eq(extract(lhs), other.extract(rhs)); }); } @@ -1744,7 +1743,7 @@ template void AgedOrderedContainer::touch( beast::detail::AgedContainerIterator pos, - typename clock_type::time_point const& now) + clock_type::time_point const& now) { auto& e(*pos.iterator()); e.when = now; diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index 7162c237d6..3bad12d9e5 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -67,8 +67,8 @@ class AgedUnorderedContainer { public: using clock_type = AbstractClock; - using time_point = typename clock_type::time_point; - using duration = typename clock_type::duration; + using time_point = clock_type::time_point; + using duration = clock_type::duration; using key_type = Key; using mapped_type = T; using value_type = std::conditional_t, Key>; @@ -99,8 +99,8 @@ private: { explicit Stashed() = default; - using value_type = typename AgedUnorderedContainer::value_type; - using time_point = typename AgedUnorderedContainer::time_point; + using value_type = AgedUnorderedContainer::value_type; + using time_point = AgedUnorderedContainer::time_point; }; Element(time_point const& when, value_type const& value) : value(value), when(when) @@ -201,8 +201,8 @@ private: } }; - using list_type = typename boost::intrusive:: - make_list>::type; + using list_type = + boost::intrusive::make_list>::type; using cont_type = std::conditional_t< IsMulti, @@ -219,16 +219,14 @@ private: boost::intrusive::equal, boost::intrusive::cache_begin>::type>; - using bucket_type = typename cont_type::bucket_type; - using bucket_traits = typename cont_type::bucket_traits; + using bucket_type = cont_type::bucket_type; + using bucket_traits = cont_type::bucket_traits; - using ElementAllocator = - typename std::allocator_traits::template rebind_alloc; + using ElementAllocator = std::allocator_traits::template rebind_alloc; using ElementAllocatorTraits = std::allocator_traits; - using BucketAllocator = - typename std::allocator_traits::template rebind_alloc; + using BucketAllocator = std::allocator_traits::template rebind_alloc; using BucketAllocatorTraits = std::allocator_traits; @@ -542,8 +540,8 @@ public: using allocator_type = Allocator; using reference = value_type&; using const_reference = value_type const&; - using pointer = typename std::allocator_traits::pointer; - using const_pointer = typename std::allocator_traits::const_pointer; + using pointer = std::allocator_traits::pointer; + using const_pointer = std::allocator_traits::const_pointer; // A set iterator (IsMap==false) is always const // because the elements of a set are immutable. @@ -850,7 +848,7 @@ public: bool MaybeMulti = IsMulti, bool MaybeMap = IsMap, class = std::enable_if_t> - typename std::conditional::type const& + std::conditional::type const& at(K const& k) const; template < @@ -1414,7 +1412,7 @@ private: void touch( beast::detail::AgedContainerIterator pos, - typename clock_type::time_point const& now) + clock_type::time_point const& now) { auto& e(*pos.iterator()); e.when = now; @@ -2111,7 +2109,7 @@ template < class KeyEqual, class Allocator> template -typename std::conditional::type const& +std::conditional::type const& AgedUnorderedContainer::at( K const& k) const { diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h index 946126a004..1c3827ae1c 100644 --- a/include/xrpl/beast/core/List.h +++ b/include/xrpl/beast/core/List.h @@ -24,7 +24,7 @@ struct CopyConst { explicit CopyConst() = default; - using type = typename std::remove_const::type const; + using type = std::remove_const::type const; }; /** @} */ @@ -56,7 +56,7 @@ class ListIterator { public: using iterator_category = std::bidirectional_iterator_tag; - using value_type = typename beast::detail::CopyConst::type; + using value_type = beast::detail::CopyConst::type; using difference_type = std::ptrdiff_t; using pointer = value_type*; using reference = value_type&; @@ -259,7 +259,7 @@ template class List { public: - using Node = typename detail::ListNode; + using Node = detail::ListNode; using value_type = T; using pointer = value_type*; diff --git a/include/xrpl/beast/core/LockFreeStack.h b/include/xrpl/beast/core/LockFreeStack.h index 6b8f686246..d4ad45cf5c 100644 --- a/include/xrpl/beast/core/LockFreeStack.h +++ b/include/xrpl/beast/core/LockFreeStack.h @@ -12,13 +12,13 @@ template class LockFreeStackIterator { protected: - using Node = typename Container::Node; + using Node = Container::Node; using NodePtr = std::conditional_t; public: using iterator_category = std::forward_iterator_tag; - using value_type = typename Container::value_type; - using difference_type = typename Container::difference_type; + using value_type = Container::value_type; + using difference_type = Container::difference_type; using pointer = std::conditional_t; using reference = std:: diff --git a/include/xrpl/beast/hash/uhash.h b/include/xrpl/beast/hash/uhash.h index 97461f67c7..9ffd5924f7 100644 --- a/include/xrpl/beast/hash/uhash.h +++ b/include/xrpl/beast/hash/uhash.h @@ -11,7 +11,7 @@ struct Uhash { Uhash() = default; - using result_type = typename Hasher::result_type; + using result_type = Hasher::result_type; template result_type diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index cf80bd26c0..e810733210 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -102,7 +102,7 @@ Result split(FwdIt first, FwdIt last, Char delim) { using namespace detail; - using string = typename Result::value_type; + using string = Result::value_type; Result result; diff --git a/include/xrpl/beast/unit_test/detail/const_container.h b/include/xrpl/beast/unit_test/detail/const_container.h index c171771e45..6826bf4258 100644 --- a/include/xrpl/beast/unit_test/detail/const_container.h +++ b/include/xrpl/beast/unit_test/detail/const_container.h @@ -32,11 +32,11 @@ protected: } public: - using value_type = typename cont_type::value_type; - using size_type = typename cont_type::size_type; - using difference_type = typename cont_type::difference_type; - using iterator = typename cont_type::const_iterator; - using const_iterator = typename cont_type::const_iterator; + using value_type = cont_type::value_type; + using size_type = cont_type::size_type; + using difference_type = cont_type::difference_type; + using iterator = cont_type::const_iterator; + using const_iterator = cont_type::const_iterator; /** Returns `true` if the container is empty. */ [[nodiscard]] bool diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index 1fdbb451a6..ff990dece5 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -48,7 +48,7 @@ private: std::size_t cases = 0; std::size_t total = 0; std::size_t failed = 0; - typename clock_type::time_point start = clock_type::now(); + clock_type::time_point start = clock_type::now(); explicit SuiteResults(std::string name = "") : name(std::move(name)) { @@ -60,7 +60,7 @@ private: struct Results { - using run_time = std::pair; + using run_time = std::pair; static constexpr auto kMaxTop = 10; @@ -69,7 +69,7 @@ private: std::size_t total = 0; std::size_t failed = 0; std::vector top; - typename clock_type::time_point start = clock_type::now(); + clock_type::time_point start = clock_type::now(); void add(SuiteResults const& r); @@ -91,7 +91,7 @@ public: private: static std::string - fmtdur(typename clock_type::duration const& d); + fmtdur(clock_type::duration const& d); void onSuiteBegin(SuiteInfo const& info) override; @@ -141,9 +141,7 @@ Reporter::Results::add(SuiteResults const& r) top.begin(), top.end(), elapsed, - [](run_time const& t1, typename clock_type::duration const& t2) { - return t1.second > t2; - }); + [](run_time const& t1, clock_type::duration const& t2) { return t1.second > t2; }); if (iter != top.end()) { if (top.size() == kMaxTop) @@ -181,7 +179,7 @@ Reporter::~Reporter() template std::string -Reporter::fmtdur(typename clock_type::duration const& d) +Reporter::fmtdur(clock_type::duration const& d) { using namespace std::chrono; auto const ms = duration_cast(d); diff --git a/include/xrpl/beast/utility/Journal.h b/include/xrpl/beast/utility/Journal.h index 1a0c148d1f..1262a64179 100644 --- a/include/xrpl/beast/utility/Journal.h +++ b/include/xrpl/beast/utility/Journal.h @@ -411,9 +411,9 @@ class BasicLogstream : public std::basic_ostream { using char_type = CharT; using traits_type = Traits; - using int_type = typename traits_type::int_type; - using pos_type = typename traits_type::pos_type; - using off_type = typename traits_type::off_type; + using int_type = traits_type::int_type; + using pos_type = traits_type::pos_type; + using off_type = traits_type::off_type; detail::LogStreamBuf buf_; diff --git a/include/xrpl/beast/utility/maybe_const.h b/include/xrpl/beast/utility/maybe_const.h index 40904471be..10b2eaf7f6 100644 --- a/include/xrpl/beast/utility/maybe_const.h +++ b/include/xrpl/beast/utility/maybe_const.h @@ -15,6 +15,6 @@ struct MaybeConst /** Alias for omitting `typename`. */ template -using maybe_const_t = typename MaybeConst::type; +using maybe_const_t = MaybeConst::type; } // namespace beast diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h index 1614a594c5..2ea84a7a3d 100644 --- a/include/xrpl/beast/utility/rngfill.h +++ b/include/xrpl/beast/utility/rngfill.h @@ -13,7 +13,7 @@ template void rngfill(void* const buffer, std::size_t const bytes, Generator& g) { - using result_type = typename Generator::result_type; + using result_type = Generator::result_type; constexpr std::size_t kResultSize = sizeof(result_type); std::uint8_t* const bufferStart = static_cast(buffer); @@ -42,7 +42,7 @@ template < void rngfill(std::array& a, Generator& g) { - using result_type = typename Generator::result_type; + using result_type = Generator::result_type; auto i = N / sizeof(result_type); result_type* p = reinterpret_cast(a.data()); while (i--) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index e9dcb8bcbe..f786c6a9dc 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -566,6 +566,7 @@ public: using SelfType = ValueConstIterator; ValueConstIterator() = default; + ValueConstIterator(ValueConstIterator const& other) = default; private: /*! \internal Use by Value to create an iterator. @@ -574,12 +575,12 @@ private: public: SelfType& - operator=(ValueIteratorBase const& other); + operator=(SelfType const& other); SelfType operator++(int) { - SelfType temp(*this); + SelfType const temp(*this); ++*this; return temp; } @@ -587,7 +588,7 @@ public: SelfType operator--(int) { - SelfType temp(*this); + SelfType const temp(*this); --*this; return temp; } diff --git a/include/xrpl/protocol/KnownFormats.h b/include/xrpl/protocol/KnownFormats.h index 9aa914ba97..6e21d4bc3a 100644 --- a/include/xrpl/protocol/KnownFormats.h +++ b/include/xrpl/protocol/KnownFormats.h @@ -118,13 +118,13 @@ public: } // begin() and end() are provided for testing purposes. - [[nodiscard]] typename std::forward_list::const_iterator + [[nodiscard]] std::forward_list::const_iterator begin() const { return formats_.begin(); } - [[nodiscard]] typename std::forward_list::const_iterator + [[nodiscard]] std::forward_list::const_iterator end() const { return formats_.end(); diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h index 87c8cd4f45..0267eac22d 100644 --- a/include/xrpl/protocol/STBitString.h +++ b/include/xrpl/protocol/STBitString.h @@ -163,7 +163,7 @@ STBitString::setValue(BaseUInt const& v) } template -typename STBitString::value_type const& +STBitString::value_type const& STBitString::value() const { return value_; diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h index 4e3c9a8923..52e0f7a365 100644 --- a/include/xrpl/protocol/STInteger.h +++ b/include/xrpl/protocol/STInteger.h @@ -120,7 +120,7 @@ STInteger::operator=(value_type const& v) } template -inline typename STInteger::value_type +inline STInteger::value_type STInteger::value() const noexcept { return value_; diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index c635e8ce22..e65cc79c78 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -243,7 +243,7 @@ public: @throws STObject::FieldErr if the field is not present. */ template - typename T::value_type + T::value_type operator[](TypedField const& f) const; /** Get the value of a field as a std::optional @@ -290,7 +290,7 @@ public: @throws STObject::FieldErr if the field is not present. */ template - [[nodiscard]] typename T::value_type + [[nodiscard]] T::value_type at(TypedField const& f) const; /** Get the value of a field as std::optional @@ -478,7 +478,7 @@ template class STObject::Proxy { public: - using value_type = typename T::value_type; + using value_type = T::value_type; [[nodiscard]] value_type value() const; @@ -513,13 +513,10 @@ protected: template concept IsArithmeticNumber = std::is_arithmetic_v || std::is_same_v || std::is_same_v; -template < - typename U, - typename Value = typename U::value_type, - typename Unit = typename U::unit_type> +template concept IsArithmeticValueUnit = std::is_same_v> && IsArithmeticNumber && std::is_class_v; -template +template concept IsArithmeticST = !IsArithmeticValueUnit && IsArithmeticNumber; template concept IsArithmetic = IsArithmeticNumber || IsArithmeticST || IsArithmeticValueUnit; @@ -534,7 +531,7 @@ template class STObject::ValueProxy : public Proxy { private: - using value_type = typename T::value_type; + using value_type = T::value_type; public: ValueProxy(ValueProxy const&) = default; @@ -576,7 +573,7 @@ template class STObject::OptionalProxy : public Proxy { private: - using value_type = typename T::value_type; + using value_type = T::value_type; using optional_type = std::optional>; @@ -840,7 +837,7 @@ operator typename STObject::OptionalProxy::optional_type() const } template -typename STObject::OptionalProxy::optional_type +STObject::OptionalProxy::optional_type STObject::OptionalProxy::operator~() const { return optionalValue(); @@ -933,7 +930,7 @@ STObject::OptionalProxy::optionalValue() const -> optional_type } template -typename STObject::OptionalProxy::value_type +STObject::OptionalProxy::value_type STObject::OptionalProxy::valueOr(value_type val) const { return engaged() ? this->value() : val; @@ -1040,7 +1037,7 @@ STObject::getPIndex(int offset) } template -typename T::value_type +T::value_type STObject::operator[](TypedField const& f) const { return at(f); @@ -1068,7 +1065,7 @@ STObject::operator[](OptionaledField const& of) -> OptionalProxy } template -[[nodiscard]] typename T::value_type +[[nodiscard]] T::value_type STObject::at(TypedField const& f) const { auto const b = peekAtPField(f); diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index c89610f354..072bd4778f 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -657,13 +657,13 @@ inline bool isTesSuccess(TER x) noexcept { // Makes use of TERSubset::operator bool() - return !(x); + return !x; } inline bool isTecClaim(TER x) noexcept { - return ((x) >= tecCLAIM); + return (x >= tecCLAIM); } std::unordered_map> const& diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 7fedc05a0d..39f745e84e 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -391,7 +391,7 @@ mulDivU(Source1 value, Dest mul, Source2 div) return std::nullopt; } - using desttype = typename Dest::value_type; + using desttype = Dest::value_type; constexpr auto kMax = std::numeric_limits::max(); // Shortcuts, since these happen a lot in the real world diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h index 457af727a2..1aec5fe549 100644 --- a/include/xrpl/protocol/XChainAttestations.h +++ b/include/xrpl/protocol/XChainAttestations.h @@ -379,16 +379,16 @@ public: [[nodiscard]] STArray toSTArray() const; - [[nodiscard]] typename AttCollection::const_iterator + [[nodiscard]] AttCollection::const_iterator begin() const; - [[nodiscard]] typename AttCollection::const_iterator + [[nodiscard]] AttCollection::const_iterator end() const; - typename AttCollection::iterator + AttCollection::iterator begin(); - typename AttCollection::iterator + AttCollection::iterator end(); template @@ -419,7 +419,7 @@ operator==( } template -inline typename XChainAttestationsBase::AttCollection const& +inline XChainAttestationsBase::AttCollection const& XChainAttestationsBase::attestations() const { return attestations_; diff --git a/include/xrpl/protocol/digest.h b/include/xrpl/protocol/digest.h index 50bf2735fb..721ce60767 100644 --- a/include/xrpl/protocol/digest.h +++ b/include/xrpl/protocol/digest.h @@ -206,7 +206,7 @@ sha512Half(Args const&... args) sha512_half_hasher h; using beast::hash_append; hash_append(h, args...); - return static_cast(h); + return static_cast(h); } /** Returns the SHA512-Half of a series of objects. @@ -222,7 +222,7 @@ sha512HalfS(Args const&... args) sha512_half_hasher_s h; using beast::hash_append; hash_append(h, args...); - return static_cast(h); + return static_cast(h); } } // namespace xrpl diff --git a/include/xrpl/shamap/FullBelowCache.h b/include/xrpl/shamap/FullBelowCache.h index 07290dfbd1..e9fd04ac58 100644 --- a/include/xrpl/shamap/FullBelowCache.h +++ b/include/xrpl/shamap/FullBelowCache.h @@ -25,7 +25,7 @@ public: static constexpr auto kDefaultCacheTargetSize = 0; using key_type = uint256; - using clock_type = typename CacheType::clock_type; + using clock_type = CacheType::clock_type; /** Construct the cache. diff --git a/src/libxrpl/json/json_valueiterator.cpp b/src/libxrpl/json/json_valueiterator.cpp index 5a3a5ffcdb..bb49902f70 100644 --- a/src/libxrpl/json/json_valueiterator.cpp +++ b/src/libxrpl/json/json_valueiterator.cpp @@ -132,7 +132,7 @@ ValueConstIterator::ValueConstIterator(Value::ObjectValues::iterator const& curr } ValueConstIterator& -ValueConstIterator::operator=(ValueIteratorBase const& other) +ValueConstIterator::operator=(SelfType const& other) { copy(other); return *this; diff --git a/src/libxrpl/protocol/XChainAttestations.cpp b/src/libxrpl/protocol/XChainAttestations.cpp index 805d08c097..792fe5da9d 100644 --- a/src/libxrpl/protocol/XChainAttestations.cpp +++ b/src/libxrpl/protocol/XChainAttestations.cpp @@ -628,28 +628,28 @@ XChainAttestationsBase::XChainAttestationsBase( } template -typename XChainAttestationsBase::AttCollection::const_iterator +XChainAttestationsBase::AttCollection::const_iterator XChainAttestationsBase::begin() const { return attestations_.begin(); } template -typename XChainAttestationsBase::AttCollection::const_iterator +XChainAttestationsBase::AttCollection::const_iterator XChainAttestationsBase::end() const { return attestations_.end(); } template -typename XChainAttestationsBase::AttCollection::iterator +XChainAttestationsBase::AttCollection::iterator XChainAttestationsBase::begin() { return attestations_.begin(); } template -typename XChainAttestationsBase::AttCollection::iterator +XChainAttestationsBase::AttCollection::iterator XChainAttestationsBase::end() { return attestations_.end(); diff --git a/src/libxrpl/protocol/tokens.cpp b/src/libxrpl/protocol/tokens.cpp index fcd822a747..d04ceaa3a6 100644 --- a/src/libxrpl/protocol/tokens.cpp +++ b/src/libxrpl/protocol/tokens.cpp @@ -135,16 +135,16 @@ static constexpr std::array const kAlphabetReverse = []() { }(); template -static typename Hasher::result_type +static Hasher::result_type digest(void const* data, std::size_t size) noexcept { Hasher h; h(data, size); - return static_cast(h); + return static_cast(h); } template > -static typename Hasher::result_type +static Hasher::result_type digest(std::array const& v) { return digest(v.data(), v.size()); @@ -152,7 +152,7 @@ digest(std::array const& v) // Computes a double digest (e.g. digest of the digest) template -static typename Hasher::result_type +static Hasher::result_type digest2(Args const&... args) { return digest(digest(args...)); diff --git a/src/libxrpl/shamap/SHAMapInnerNode.cpp b/src/libxrpl/shamap/SHAMapInnerNode.cpp index ee6ebf7f3f..74a0e4515f 100644 --- a/src/libxrpl/shamap/SHAMapInnerNode.cpp +++ b/src/libxrpl/shamap/SHAMapInnerNode.cpp @@ -200,7 +200,7 @@ SHAMapInnerNode::updateHash() using beast::hash_append; hash_append(h, HashPrefix::InnerNode); iterChildren([&](SHAMapHash const& hh) { hash_append(h, hh); }); - nh = static_cast(h); + nh = static_cast(h); } hash_ = SHAMapHash{nh}; } diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index b4a533905c..e29a9fe661 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -407,8 +407,10 @@ AccountRootsNotDeleted::finalize( "succeeded without deleting an account"; } else + { JLOG(j.fatal()) << "Invariant failed: account deletion " "succeeded but deleted multiple accounts!"; + } return false; } diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index 4b045521d2..448172872a 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -1477,8 +1477,8 @@ bookStepEqual(Step const& step, xrpl::Book const& book) { return std::visit( [&](TIn const&, TOut const&) { - using TIn_ = typename TIn::amount_type; - using TOut_ = typename TOut::amount_type; + using TIn_ = TIn::amount_type; + using TOut_ = TOut::amount_type; if constexpr (ValidTaker) { diff --git a/src/libxrpl/tx/paths/Flow.cpp b/src/libxrpl/tx/paths/Flow.cpp index 7be1f9f633..80e05f058c 100644 --- a/src/libxrpl/tx/paths/Flow.cpp +++ b/src/libxrpl/tx/paths/Flow.cpp @@ -125,8 +125,8 @@ flow( // amount types. return std::visit( [&, &strands = strands](TIn const&, TOut const&) { - using TIn_ = typename TIn::amount_type; - using TOut_ = typename TOut::amount_type; + using TIn_ = TIn::amount_type; + using TOut_ = TOut::amount_type; return finishFlow( sb, srcAsset, diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index 5be00fe76c..be24b6326a 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -120,7 +120,7 @@ TicketCreate::doApply() } // Update the record of the number of Tickets this account owns. - std::uint32_t const oldTicketCount = (*(sleAccountRoot))[~sfTicketCount].valueOr(0u); + std::uint32_t const oldTicketCount = (*sleAccountRoot)[~sfTicketCount].valueOr(0u); sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 810d93e6e1..2422db05de 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -1077,10 +1077,10 @@ struct LedgerReplayer_test : public beast::unit_test::Suite { Config c; - std::string const toLoad = (R"xrpldConfig( + std::string const toLoad = R"xrpldConfig( [ledger_replay] 0 -)xrpldConfig"); +)xrpldConfig"; c.loadFromString(toLoad); BEAST_EXPECT(c.ledgerReplay == false); } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index dbb0033368..2d70efd9b2 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -3006,9 +3006,9 @@ protected: } if (mptTest) - (mptTest)(env, brokers[0], mptt); + mptTest(env, brokers[0], mptt); if (iouTest) - (iouTest)(env, brokers[1]); + iouTest(env, brokers[1]); }; testCase( diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 117f2bc816..e24a524b81 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -143,7 +143,7 @@ class NFTokenDir_test : public beast::unit_test::Suite for (uint256 const& nftID : nftIDs) { offers.emplace_back(keylet::nftoffer(issuer, env.seq(issuer)).key); - env(token::createOffer(issuer, nftID, XRP(0)), Txflags((tfSellNFToken))); + env(token::createOffer(issuer, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); } @@ -217,7 +217,7 @@ class NFTokenDir_test : public beast::unit_test::Suite offers.emplace_back(keylet::nftoffer(account, env.seq(account)).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), - Txflags((tfSellNFToken))); + Txflags(tfSellNFToken)); } env.close(); @@ -421,7 +421,7 @@ class NFTokenDir_test : public beast::unit_test::Suite offers.emplace_back(keylet::nftoffer(account, env.seq(account)).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), - Txflags((tfSellNFToken))); + Txflags(tfSellNFToken)); } env.close(); @@ -651,7 +651,7 @@ class NFTokenDir_test : public beast::unit_test::Suite offers.emplace_back(keylet::nftoffer(account, env.seq(account)).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), - Txflags((tfSellNFToken))); + Txflags(tfSellNFToken)); } env.close(); @@ -823,7 +823,7 @@ class NFTokenDir_test : public beast::unit_test::Suite offers[i].emplace_back(keylet::nftoffer(account, env.seq(account)).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), - Txflags((tfSellNFToken))); + Txflags(tfSellNFToken)); } } env.close(); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index ed0b2ffbe3..ac50924ac2 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -3431,7 +3431,7 @@ public: auto const gw = Account("gateway"); auto const fee = env.current()->fees().base; - env.fund(reserve(env, 2) + drops(9999640) + (fee), ann); + env.fund(reserve(env, 2) + drops(9999640) + fee, ann); env.fund(reserve(env, 2) + (fee * 4), gw); env.close(); @@ -3467,7 +3467,7 @@ public: auto const bob = Account("bob"); auto const fee = env.current()->fees().base; - env.fund(reserve(env, 2) + drops(400'000'000'000) + (fee), alice, bob); + env.fund(reserve(env, 2) + drops(400'000'000'000) + fee, alice, bob); env.fund(reserve(env, 2) + (fee * 4), gw); env.close(); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index ea8b0a7c0e..1a2f0b2b12 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -3621,7 +3621,7 @@ public: auto const btc = gw["BTC"]; auto const fee = env.current()->fees().base; - env.fund(reserve(env, 2) + drops(9999640) + (fee), ann); + env.fund(reserve(env, 2) + drops(9999640) + fee, ann); env.fund(reserve(env, 2) + (fee * 4), gw); env.close(); @@ -3659,7 +3659,7 @@ public: auto const cny = gw["CNY"]; auto const fee = env.current()->fees().base; - env.fund(reserve(env, 2) + drops(400000000000) + (fee), alice, bob); + env.fund(reserve(env, 2) + drops(400000000000) + fee, alice, bob); env.fund(reserve(env, 2) + (fee * 4), gw); env.close(); @@ -3706,7 +3706,7 @@ public: auto const jpy = gw["JPY"]; auto const fee = env.current()->fees().base; - env.fund(reserve(env, 2) + drops(400000000000) + (fee), alice, bob); + env.fund(reserve(env, 2) + drops(400000000000) + fee, alice, bob); env.fund(reserve(env, 2) + (fee * 4), gw); env.close(); @@ -3759,7 +3759,7 @@ public: auto const jpy = gw2["JPY"]; auto const fee = env.current()->fees().base; - env.fund(reserve(env, 2) + drops(400000000000) + (fee), alice, bob); + env.fund(reserve(env, 2) + drops(400000000000) + fee, alice, bob); env.fund(reserve(env, 2) + (fee * 4), gw1, gw2); env.close(); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 0ae6b4d80a..97cb035578 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -1176,7 +1176,7 @@ public: // bankrupt Alice. Fails, because an account can't have // more than the minimum reserve in flight before the // last queued transaction - aliceFee = env.le(alice)->getFieldAmount(sfBalance).xrp().drops() - (62); + aliceFee = env.le(alice)->getFieldAmount(sfBalance).xrp().drops() - 62; env(noop(alice), Seq(aliceSeq), Fee(aliceFee), Ter(telCAN_NOT_QUEUE_BALANCE)); checkMetrics(*this, env, 4, 10, 6, 5); diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp index d7f74aaa7d..2ac5fc5a33 100644 --- a/src/test/beast/aged_associative_container_test.cpp +++ b/src/test/beast/aged_associative_container_test.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -232,10 +233,10 @@ public: { public: using T = void; - using Value = typename Base::Key; + using Value = Base::Key; using Values = std::vector; - static typename Base::Key const& + static Base::Key const& extract(Value const& value) { return value; // NOLINT(bugprone-return-const-ref-from-parameter) @@ -271,7 +272,7 @@ public: using Value = std::pair; using Values = std::vector; - static typename Base::Key const& + static Base::Key const& extract(Value const& value) { return value.first; @@ -387,7 +388,7 @@ public: struct EqualValue { bool - operator()(typename Traits::Value const& lhs, typename Traits::Value const& rhs) + operator()(Traits::Value const& lhs, Traits::Value const& rhs) { return Traits::extract(lhs) == Traits::extract(rhs); } @@ -647,7 +648,7 @@ AgedAssociativeContainerTestBase::checkUnorderedContentsRefRef(C&& c, Values con using Cont = std::remove_reference_t; using Traits = TestTraits; - using size_type = typename Cont::size_type; + using size_type = Cont::size_type; auto const hash(c.hashFunction()); auto const keyEq(c.keyEq()); for (size_type i(0); i < c.bucketCount(); ++i) @@ -655,10 +656,9 @@ AgedAssociativeContainerTestBase::checkUnorderedContentsRefRef(C&& c, Values con auto const last(c.end(i)); for (auto iter(c.begin(i)); iter != last; ++iter) { - auto const match( - std::find_if(v.begin(), v.end(), [iter](typename Values::value_type const& e) { - return Traits::extract(*iter) == Traits::extract(e); - })); + auto const match(std::ranges::find_if(v, [iter](Values::value_type const& e) { + return Traits::extract(*iter) == Traits::extract(e); + })); BEAST_EXPECT(match != v.end()); BEAST_EXPECT(keyEq(Traits::extract(*iter), Traits::extract(*match))); BEAST_EXPECT(hash(Traits::extract(*iter)) == hash(Traits::extract(*match))); @@ -671,7 +671,7 @@ void AgedAssociativeContainerTestBase::checkContentsRefRef(C&& c, Values const& v) { using Cont = std::remove_reference_t; - using size_type = typename Cont::size_type; + using size_type = Cont::size_type; BEAST_EXPECT(c.size() == v.size()); BEAST_EXPECT(size_type(std::distance(c.begin(), c.end())) == v.size()); @@ -703,7 +703,7 @@ AgedAssociativeContainerTestBase::checkContents(Cont& c) { using Traits = TestTraits; - using Values = typename Traits::Values; + using Values = Traits::Values; checkContents(c, Values()); } @@ -719,10 +719,10 @@ std::enable_if_t AgedAssociativeContainerTestBase::testConstructEmpty() { using Traits = TestTraits; - using Comp = typename Traits::Comp; - using Alloc = typename Traits::Alloc; - using MyComp = typename Traits::MyComp; - using MyAlloc = typename Traits::MyAlloc; + using Comp = Traits::Comp; + using Alloc = Traits::Alloc; + using MyComp = Traits::MyComp; + using MyAlloc = Traits::MyAlloc; typename Traits::ManualClock clock; // testcase (Traits::name() + " empty"); @@ -755,12 +755,12 @@ std::enable_if_t AgedAssociativeContainerTestBase::testConstructEmpty() { using Traits = TestTraits; - using Hash = typename Traits::Hash; - using Equal = typename Traits::Equal; - using Alloc = typename Traits::Alloc; - using MyHash = typename Traits::MyHash; - using MyEqual = typename Traits::MyEqual; - using MyAlloc = typename Traits::MyAlloc; + using Hash = Traits::Hash; + using Equal = Traits::Equal; + using Alloc = Traits::Alloc; + using MyHash = Traits::MyHash; + using MyEqual = Traits::MyEqual; + using MyAlloc = Traits::MyAlloc; typename Traits::ManualClock clock; // testcase (Traits::name() + " empty"); @@ -813,10 +813,10 @@ std::enable_if_t AgedAssociativeContainerTestBase::testConstructRange() { using Traits = TestTraits; - using Comp = typename Traits::Comp; - using Alloc = typename Traits::Alloc; - using MyComp = typename Traits::MyComp; - using MyAlloc = typename Traits::MyAlloc; + using Comp = Traits::Comp; + using Alloc = Traits::Alloc; + using MyComp = Traits::MyComp; + using MyAlloc = Traits::MyAlloc; typename Traits::ManualClock clock; auto const v(Traits::values()); @@ -860,12 +860,12 @@ std::enable_if_t AgedAssociativeContainerTestBase::testConstructRange() { using Traits = TestTraits; - using Hash = typename Traits::Hash; - using Equal = typename Traits::Equal; - using Alloc = typename Traits::Alloc; - using MyHash = typename Traits::MyHash; - using MyEqual = typename Traits::MyEqual; - using MyAlloc = typename Traits::MyAlloc; + using Hash = Traits::Hash; + using Equal = Traits::Equal; + using Alloc = Traits::Alloc; + using MyHash = Traits::MyHash; + using MyEqual = Traits::MyEqual; + using MyAlloc = Traits::MyAlloc; typename Traits::ManualClock clock; auto const v(Traits::values()); @@ -962,7 +962,7 @@ void AgedAssociativeContainerTestBase::testCopyMove() { using Traits = TestTraits; - using Alloc = typename Traits::Alloc; + using Alloc = Traits::Alloc; typename Traits::ManualClock clock; auto const v(Traits::values()); @@ -1307,7 +1307,7 @@ AgedAssociativeContainerTestBase::testChronological() // Test touch() with a non-const iterator. for (auto iter(v.crbegin()); iter != v.crend(); ++iter) { - using iterator = typename decltype(c)::iterator; + using iterator = decltype(c)::iterator; iterator const found(c.find(Traits::extract(*iter))); BEAST_EXPECT(found != c.cend()); @@ -1327,7 +1327,7 @@ AgedAssociativeContainerTestBase::testChronological() // Test touch() with a const_iterator for (auto iter(v.cbegin()); iter != v.cend(); ++iter) { - using const_iterator = typename decltype(c)::const_iterator; + using const_iterator = decltype(c)::const_iterator; const_iterator const found(c.find(Traits::extract(*iter))); BEAST_EXPECT(found != c.cend()); diff --git a/src/test/beast/beast_io_latency_probe_test.cpp b/src/test/beast/beast_io_latency_probe_test.cpp index 5f183ef091..b7e4980f05 100644 --- a/src/test/beast/beast_io_latency_probe_test.cpp +++ b/src/test/beast/beast_io_latency_probe_test.cpp @@ -36,8 +36,8 @@ class io_latency_probe_test : public beast::unit_test::Suite, public beast::test template struct MeasureAsioTimers { - using duration = typename Clock::duration; - using rep = typename MeasureClock::duration::rep; + using duration = Clock::duration; + using rep = MeasureClock::duration::rep; std::vector elapsedTimes; diff --git a/src/test/consensus/ByzantineFailureSim_test.cpp b/src/test/consensus/ByzantineFailureSim_test.cpp index ad75a78086..c3c51125b5 100644 --- a/src/test/consensus/ByzantineFailureSim_test.cpp +++ b/src/test/consensus/ByzantineFailureSim_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/consensus/Consensus_test.cpp b/src/test/consensus/Consensus_test.cpp index 629a97f0ea..92a4c67e32 100644 --- a/src/test/consensus/Consensus_test.cpp +++ b/src/test/consensus/Consensus_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/consensus/DistributedValidatorsSim_test.cpp b/src/test/consensus/DistributedValidatorsSim_test.cpp index 6d9ac6bede..1def09db13 100644 --- a/src/test/consensus/DistributedValidatorsSim_test.cpp +++ b/src/test/consensus/DistributedValidatorsSim_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/consensus/ScaleFreeSim_test.cpp b/src/test/consensus/ScaleFreeSim_test.cpp index 7e75aea72a..e533e09eb0 100644 --- a/src/test/consensus/ScaleFreeSim_test.cpp +++ b/src/test/consensus/ScaleFreeSim_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/csf/BasicNetwork.h b/src/test/csf/BasicNetwork.h index e1d519b30b..418cdcf289 100644 --- a/src/test/csf/BasicNetwork.h +++ b/src/test/csf/BasicNetwork.h @@ -64,9 +64,9 @@ class BasicNetwork using clock_type = Scheduler::clock_type; - using duration = typename clock_type::duration; + using duration = clock_type::duration; - using time_point = typename clock_type::time_point; + using time_point = clock_type::time_point; struct LinkType { diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h index 6fbb8c3514..20e45faa5b 100644 --- a/src/test/csf/Digraph.h +++ b/src/test/csf/Digraph.h @@ -128,7 +128,7 @@ public: outVertices() const { return boost::adaptors::transform( - graph_, [](typename Graph::value_type const& v) { return v.first; }); + graph_, [](Graph::value_type const& v) { return v.first; }); } /** Range over target vertices @@ -139,7 +139,7 @@ public: [[nodiscard]] auto outVertices(Vertex source) const { - auto transform = [](typename Links::value_type const& link) { return link.first; }; + auto transform = [](Links::value_type const& link) { return link.first; }; auto it = graph_.find(source); if (it != graph_.end()) return boost::adaptors::transform(it->second, transform); @@ -165,7 +165,7 @@ public: [[nodiscard]] auto outEdges(Vertex source) const { - auto transform = [source](typename Links::value_type const& link) { + auto transform = [source](Links::value_type const& link) { return Edge{source, link.first, link.second}; }; diff --git a/src/test/csf/Scheduler.h b/src/test/csf/Scheduler.h index ede43be854..e7cbe27036 100644 --- a/src/test/csf/Scheduler.h +++ b/src/test/csf/Scheduler.h @@ -27,9 +27,9 @@ class Scheduler public: using clock_type = beast::ManualClock; - using duration = typename clock_type::duration; + using duration = clock_type::duration; - using time_point = typename clock_type::time_point; + using time_point = clock_type::time_point; private: using by_when_hook = @@ -87,14 +87,14 @@ private: class QueueType { private: - using by_when_set = typename boost::intrusive:: + using by_when_set = boost::intrusive:: make_multiset>::type; // alloc_ is owned by the scheduler boost::container::pmr::monotonic_buffer_resource* alloc_; by_when_set byWhen_; public: - using iterator = typename by_when_set::iterator; + using iterator = by_when_set::iterator; QueueType(QueueType const&) = delete; QueueType& @@ -114,7 +114,7 @@ private: end(); template - typename by_when_set::iterator + by_when_set::iterator emplace(time_point when, Handler&& h); iterator @@ -287,7 +287,7 @@ Scheduler::QueueType::end() -> iterator template inline auto -Scheduler::QueueType::emplace(time_point when, Handler&& h) -> typename by_when_set::iterator +Scheduler::QueueType::emplace(time_point when, Handler&& h) -> by_when_set::iterator { using event_type = EventImpl>; auto const p = alloc_->allocate(sizeof(event_type)); @@ -296,7 +296,7 @@ Scheduler::QueueType::emplace(time_point when, Handler&& h) -> typename by_when_ } inline auto -Scheduler::QueueType::erase(iterator iter) -> typename by_when_set::iterator +Scheduler::QueueType::erase(iterator iter) -> by_when_set::iterator { auto& e = *iter; auto next = byWhen_.erase(iter); @@ -309,7 +309,7 @@ Scheduler::QueueType::erase(iterator iter) -> typename by_when_set::iterator struct Scheduler::CancelToken { private: - typename QueueType::iterator iter_; + QueueType::iterator iter_; public: CancelToken() = delete; @@ -319,7 +319,7 @@ public: private: friend class Scheduler; - CancelToken(typename QueueType::iterator iter) : iter_(iter) + CancelToken(QueueType::iterator iter) : iter_(iter) { } }; diff --git a/src/test/csf/SimTime.h b/src/test/csf/SimTime.h index e38a375e02..125674f15d 100644 --- a/src/test/csf/SimTime.h +++ b/src/test/csf/SimTime.h @@ -11,7 +11,7 @@ using RealDuration = RealClock::duration; using RealTime = RealClock::time_point; using SimClock = beast::ManualClock; -using SimDuration = typename SimClock::duration; -using SimTime = typename SimClock::time_point; +using SimDuration = SimClock::duration; +using SimTime = SimClock::time_point; } // namespace xrpl::test::csf diff --git a/src/test/csf/random.h b/src/test/csf/random.h index 08002aed7f..30f98e37fe 100644 --- a/src/test/csf/random.h +++ b/src/test/csf/random.h @@ -72,14 +72,14 @@ public: Selector(RAIter first, RAIter last, std::vector const& w, Generator& g) : first_{first}, last_{last}, dd_{w.begin(), w.end()}, g_{g} { - using tag = typename std::iterator_traits::iterator_category; + using tag = std::iterator_traits::iterator_category; static_assert( std::is_same_v, "Selector only supports random access iterators."); // TODO: Allow for forward iterators } - typename std::iterator_traits::value_type + std::iterator_traits::value_type operator()() { auto idx = dd_(g_); diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 27c54d830b..0e35e6b9ec 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -27,6 +27,7 @@ namespace xrpl::test::jtx { */ template < class SField, + // NOLINTNEXTLINE(readability-redundant-typename): typename required by MSVC class StoredValue = typename SField::type::value_type, class OutputValue = StoredValue> struct JTxField @@ -213,8 +214,8 @@ template struct JTxFieldWrapper { using JF = JTxField; - using SF = typename JF::SF; - using SV = typename JF::SV; + using SF = JF::SF; + using SV = JF::SV; protected: SF const& sfield_; @@ -266,9 +267,11 @@ public: } }; +// NOLINTNEXTLINE(readability-redundant-typename): typename required by MSVC template using valueUnitWrapper = JTxFieldWrapper>; +// NOLINTNEXTLINE(readability-redundant-typename): typename required by MSVC template using simpleField = JTxFieldWrapper>; diff --git a/src/test/nodestore/Timing_test.cpp b/src/test/nodestore/Timing_test.cpp index f5e6bf8aa4..e308d0d5ff 100644 --- a/src/test/nodestore/Timing_test.cpp +++ b/src/test/nodestore/Timing_test.cpp @@ -57,7 +57,7 @@ template static void rngcpy(void* buffer, std::size_t bytes, Generator& g) { - using result_type = typename Generator::result_type; + using result_type = Generator::result_type; while (bytes >= sizeof(result_type)) { auto const v = g(); diff --git a/src/test/server/Server_test.cpp b/src/test/server/Server_test.cpp index 54091ef767..9ff0015b5d 100644 --- a/src/test/server/Server_test.cpp +++ b/src/test/server/Server_test.cpp @@ -169,7 +169,7 @@ public: // Connect to an address template bool - connect(Socket& s, typename Socket::endpoint_type const& ep) + connect(Socket& s, Socket::endpoint_type const& ep) { try { diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 3a56d22654..71208313a4 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -69,9 +69,7 @@ Results::add(SuiteResults const& r) top.begin(), top.end(), elapsed, - [](run_time const& t1, typename clock_type::duration const& t2) { - return t1.second > t2; - }); + [](run_time const& t1, clock_type::duration const& t2) { return t1.second > t2; }); if (iter != top.end()) { diff --git a/src/test/unit_test/multi_runner.h b/src/test/unit_test/multi_runner.h index 8b07559e8c..55cf6c25fa 100644 --- a/src/test/unit_test/multi_runner.h +++ b/src/test/unit_test/multi_runner.h @@ -42,7 +42,7 @@ struct SuiteResults std::size_t cases = 0; std::size_t total = 0; std::size_t failed = 0; - typename clock_type::time_point start = clock_type::now(); + clock_type::time_point start = clock_type::now(); explicit SuiteResults(std::string name = "") : name(std::move(name)) { @@ -57,7 +57,7 @@ struct Results using static_string = boost::beast::static_string<256>; // results may be stored in shared memory. Use `static_string` to ensure // pointers from different memory spaces do not co-mingle - using run_time = std::pair; + using run_time = std::pair; static constexpr auto kMaxTop = 10; @@ -66,7 +66,7 @@ struct Results std::size_t total = 0; std::size_t failed = 0; boost::container::static_vector top; - typename clock_type::time_point start = clock_type::now(); + clock_type::time_point start = clock_type::now(); void add(SuiteResults const& r); diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index a474c9c339..a7c2b26c04 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -580,7 +580,9 @@ RCLConsensus::Adaptor::doAccept( JLOG(j_.info()) << "CNF Val " << newLCLHash; } else + { JLOG(j_.info()) << "CNF buildLCL " << newLCLHash; + } // See if we can accept a ledger as fully-validated ledgerMaster_.consensusBuilt(built.ledger, result.txns.id(), std::move(consensusJson)); @@ -796,7 +798,9 @@ RCLConsensus::Adaptor::buildLCL( JLOG(j_.debug()) << "Consensus built ledger we were acquiring"; } else + { JLOG(j_.debug()) << "Consensus built new ledger"; + } return RCLCxLedger{std::move(built)}; } diff --git a/src/xrpld/app/consensus/RCLValidations.cpp b/src/xrpld/app/consensus/RCLValidations.cpp index 02ff86b23d..9d40e60b00 100644 --- a/src/xrpld/app/consensus/RCLValidations.cpp +++ b/src/xrpld/app/consensus/RCLValidations.cpp @@ -46,8 +46,10 @@ RCLValidatedLedger::RCLValidatedLedger( ancestors_ = hashIndex->getFieldV256(sfHashes).value(); } else + { JLOG(j_.warn()) << "Ledger " << ledgerSeq_ << ":" << ledgerID_ << " missing recent ancestor hashes"; + } } auto diff --git a/src/xrpld/app/ledger/LedgerHistory.cpp b/src/xrpld/app/ledger/LedgerHistory.cpp index 8520fc941f..b7e1772942 100644 --- a/src/xrpld/app/ledger/LedgerHistory.cpp +++ b/src/xrpld/app/ledger/LedgerHistory.cpp @@ -368,8 +368,10 @@ LedgerHistory::handleMismatch( << " validated: " << to_string(*validatedConsensusHash); } else + { JLOG(j_.error()) << "MISMATCH with same consensus transaction set: " << to_string(*builtConsensusHash); + } } // Find differences between built and valid ledgers @@ -381,8 +383,10 @@ LedgerHistory::handleMismatch( JLOG(j_.error()) << "MISMATCH with same " << builtTx.size() << " transactions"; } else + { JLOG(j_.error()) << "MISMATCH with " << builtTx.size() << " built and " << validTx.size() << " valid transactions."; + } JLOG(j_.error()) << "built\n" << getJson({*builtLedger, {}}); JLOG(j_.error()) << "valid\n" << getJson({*validLedger, {}}); diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp index a77c1c9c50..2a4121ede9 100644 --- a/src/xrpld/app/ledger/detail/BuildLedger.cpp +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp @@ -204,9 +204,11 @@ buildLedger( << accum.txCount(); } else + { JLOG(j.debug()) << "Applied " << applied << " transactions. " << "Total transactions in ledger (including Inner Batch): " << accum.txCount(); + } }); } diff --git a/src/xrpld/app/ledger/detail/LedgerCleaner.cpp b/src/xrpld/app/ledger/detail/LedgerCleaner.cpp index 9f2db9d2f2..b96f01e577 100644 --- a/src/xrpld/app/ledger/detail/LedgerCleaner.cpp +++ b/src/xrpld/app/ledger/detail/LedgerCleaner.cpp @@ -366,7 +366,9 @@ private: } } else + { JLOG(j_.warn()) << "Validated ledger is prior to target ledger"; + } return ledgerHash; } diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 9baad0ec90..31510b44d2 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -755,7 +755,9 @@ LedgerMaster::getFetchPack(LedgerIndex missing, InboundLedger::Reason reason) JLOG(journal_.trace()) << "Requested fetch pack for " << missing; } else + { JLOG(journal_.debug()) << "No peer for fetch pack"; + } } void @@ -1797,10 +1799,14 @@ LedgerMaster::fetchForHistory( getFetchPack(missing, reason); } else + { JLOG(journal_.trace()) << "fetchForHistory no fetch pack for " << missing; + } } else + { JLOG(journal_.debug()) << "fetchForHistory found failed acquire"; + } } if (ledger) { diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 67b5e30eb7..c2fb78ce79 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -1612,7 +1612,9 @@ ApplicationImp::signalStop(std::string const& msg) JLOG(journal_.warn()) << "Server stopping"; } else + { JLOG(journal_.warn()) << "Server stopping: " << msg; + } isTimeToStop.notify_all(); } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index d807dea10a..1802441a66 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1687,11 +1687,13 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) e.transaction->setKept(); } else + { JLOG(journal_.debug()) << "Not holding transaction " << e.transaction->getID() << ": " << (e.local ? "local" : "network") << ", " << "result: " << e.result << " ledgers left: " << (ledgersLeft ? to_string(*ledgersLeft) : "unspecified"); + } } } else diff --git a/src/xrpld/app/misc/detail/Transaction.cpp b/src/xrpld/app/misc/detail/Transaction.cpp index 425a5723fb..2c55c474eb 100644 --- a/src/xrpld/app/misc/detail/Transaction.cpp +++ b/src/xrpld/app/misc/detail/Transaction.cpp @@ -73,7 +73,7 @@ Transaction::setStatus( TransStatus Transaction::sqlTransactionStatus(boost::optional const& status) { - auto const c = (status) ? safeCast((*status)[0]) : TxnSql::Unknown; + auto const c = status ? safeCast((*status)[0]) : TxnSql::Unknown; switch (static_cast(c)) { diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp b/src/xrpld/app/misc/detail/ValidatorSite.cpp index 76fd078174..6ce3711652 100644 --- a/src/xrpld/app/misc/detail/ValidatorSite.cpp +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp @@ -339,8 +339,10 @@ ValidatorSite::onRequestTimeout(std::size_t siteIdx, error_code const& ec) JLOG(j_.warn()) << "Request for " << site.activeResource->uri << " took too long"; } else + { JLOG(j_.error()) << "Request took too long, but a response has " "already been processed"; + } } std::scoped_lock const lockState{stateMutex_}; diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp index 9b7db6f0f5..dcc146397b 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.cpp +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp @@ -125,7 +125,7 @@ makeLedgerDBs( std::size_t notnull = 0, dfltValue = 0, pk = 0; soci::indicator ind = soci::i_null; soci::statement st = - (tx->getSession().prepare << ("PRAGMA table_info(AccountTransactions);"), + (tx->getSession().prepare << "PRAGMA table_info(AccountTransactions);", soci::into(cid), soci::into(name), soci::into(type), @@ -1065,10 +1065,10 @@ accountTxPage( if (findLedger == 0) { sql = boost::str( - boost::format(kPrefix + (R"(AccountTransactions.LedgerSeq BETWEEN %u AND %u + boost::format(kPrefix + R"(AccountTransactions.LedgerSeq BETWEEN %u AND %u ORDER BY AccountTransactions.LedgerSeq %s, AccountTransactions.TxnSeq %s - LIMIT %u;)")) % + LIMIT %u;)") % toBase58(options.account) % options.ledgerRange.min % options.ledgerRange.max % order % order % queryLimit); } @@ -1080,7 +1080,7 @@ accountTxPage( auto b58acct = toBase58(options.account); sql = boost::str( - boost::format(( + boost::format( R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq, Status,RawTxn,TxnMeta FROM AccountTransactions, Transactions WHERE @@ -1097,7 +1097,7 @@ accountTxPage( ORDER BY AccountTransactions.LedgerSeq %s, AccountTransactions.TxnSeq %s LIMIT %u; - )")) % + )") % b58acct % minLedger % maxLedger % b58acct % findLedger % compare % findSeq % order % order % queryLimit); } diff --git a/src/xrpld/consensus/Consensus.h b/src/xrpld/consensus/Consensus.h index 131db30ce0..b8d04e18b5 100644 --- a/src/xrpld/consensus/Consensus.h +++ b/src/xrpld/consensus/Consensus.h @@ -276,11 +276,11 @@ checkConsensus( template class Consensus { - using Ledger_t = typename Adaptor::Ledger_t; - using TxSet_t = typename Adaptor::TxSet_t; - using NodeID_t = typename Adaptor::NodeID_t; - using Tx_t = typename TxSet_t::Tx; - using PeerPosition_t = typename Adaptor::PeerPosition_t; + using Ledger_t = Adaptor::Ledger_t; + using TxSet_t = Adaptor::TxSet_t; + using NodeID_t = Adaptor::NodeID_t; + using Tx_t = TxSet_t::Tx; + using PeerPosition_t = Adaptor::PeerPosition_t; using Proposal_t = ConsensusProposal; using Result = ConsensusResult; @@ -341,7 +341,7 @@ public: void startRound( NetClock::time_point const& now, - typename Ledger_t::ID const& prevLedgerID, + Ledger_t::ID const& prevLedgerID, Ledger_t prevLedger, hash_set const& nowUntrusted, bool proposing, @@ -402,7 +402,7 @@ public: @return ID of previous ledger */ - typename Ledger_t::ID + Ledger_t::ID prevLedgerID() const { return prevLedgerID_; @@ -428,16 +428,14 @@ private: void startRoundInternal( NetClock::time_point const& now, - typename Ledger_t::ID const& prevLedgerID, + Ledger_t::ID const& prevLedgerID, Ledger_t const& prevLedger, ConsensusMode mode, std::unique_ptr const& clog); // Change our view of the previous ledger void - handleWrongLedger( - typename Ledger_t::ID const& lgrId, - std::unique_ptr const& clog); + handleWrongLedger(Ledger_t::ID const& lgrId, std::unique_ptr const& clog); /** Check if our previous ledger matches the network's. @@ -568,7 +566,7 @@ private: // Non-peer (self) consensus data // Last validated ledger ID provided to consensus - typename Ledger_t::ID prevLedgerID_; + Ledger_t::ID prevLedgerID_; // Last validated ledger seen by consensus Ledger_t previousLedger_; @@ -616,7 +614,7 @@ template void Consensus::startRound( NetClock::time_point const& now, - typename Ledger_t::ID const& prevLedgerID, + Ledger_t::ID const& prevLedgerID, Ledger_t prevLedger, hash_set const& nowUntrusted, bool proposing, @@ -661,7 +659,7 @@ template void Consensus::startRoundInternal( NetClock::time_point const& now, - typename Ledger_t::ID const& prevLedgerID, + Ledger_t::ID const& prevLedgerID, Ledger_t const& prevLedger, ConsensusMode mode, std::unique_ptr const& clog) @@ -811,7 +809,9 @@ Consensus::peerProposalInternal( gotTxSet(now_, *set); } else + { JLOG(j_.debug()) << "Don't have tx set for peer"; + } } else if (result_) { @@ -1025,7 +1025,7 @@ Consensus::getJson(bool full) const template void Consensus::handleWrongLedger( - typename Ledger_t::ID const& lgrId, + Ledger_t::ID const& lgrId, std::unique_ptr const& clog) { CLOG(clog) << "handleWrongLedger. "; diff --git a/src/xrpld/consensus/ConsensusTypes.h b/src/xrpld/consensus/ConsensusTypes.h index 64a7f5fdea..f043fc0663 100644 --- a/src/xrpld/consensus/ConsensusTypes.h +++ b/src/xrpld/consensus/ConsensusTypes.h @@ -183,11 +183,11 @@ enum class ConsensusState { template struct ConsensusResult { - using Ledger_t = typename Traits::Ledger_t; - using TxSet_t = typename Traits::TxSet_t; - using NodeID_t = typename Traits::NodeID_t; + using Ledger_t = Traits::Ledger_t; + using TxSet_t = Traits::TxSet_t; + using NodeID_t = Traits::NodeID_t; - using Tx_t = typename TxSet_t::Tx; + using Tx_t = TxSet_t::Tx; using Proposal_t = ConsensusProposal; using Dispute_t = DisputedTx; diff --git a/src/xrpld/consensus/DisputedTx.h b/src/xrpld/consensus/DisputedTx.h index 1c0c069f54..ba8329714b 100644 --- a/src/xrpld/consensus/DisputedTx.h +++ b/src/xrpld/consensus/DisputedTx.h @@ -29,7 +29,7 @@ namespace xrpl { template class DisputedTx { - using TxID_t = typename Tx::ID; + using TxID_t = Tx::ID; using Map_t = boost::container::flat_map; public: diff --git a/src/xrpld/consensus/LedgerTrie.h b/src/xrpld/consensus/LedgerTrie.h index 9d76c7f283..cd9662ff02 100644 --- a/src/xrpld/consensus/LedgerTrie.h +++ b/src/xrpld/consensus/LedgerTrie.h @@ -21,8 +21,8 @@ template class SpanTip { public: - using Seq = typename Ledger::Seq; - using ID = typename Ledger::ID; + using Seq = Ledger::Seq; + using ID = Ledger::ID; SpanTip(Seq s, ID i, Ledger const lgr) : seq{s}, id{i}, ledger_{std::move(lgr)} { @@ -58,8 +58,8 @@ namespace ledger_trie_detail { template class Span { - using Seq = typename Ledger::Seq; - using ID = typename Ledger::ID; + using Seq = Ledger::Seq; + using ID = Ledger::ID; // The span is the half-open interval [start,end) of ledger_ Seq start_{0}; @@ -323,8 +323,8 @@ struct Node template class LedgerTrie { - using Seq = typename Ledger::Seq; - using ID = typename Ledger::ID; + using Seq = Ledger::Seq; + using ID = Ledger::ID; using Node = ledger_trie_detail::Node; using Span = ledger_trie_detail::Span; diff --git a/src/xrpld/consensus/Validations.h b/src/xrpld/consensus/Validations.h index 2f5762ce83..f109ae620b 100644 --- a/src/xrpld/consensus/Validations.h +++ b/src/xrpld/consensus/Validations.h @@ -267,13 +267,13 @@ to_string(ValStatus m) template class Validations { - using Mutex = typename Adaptor::Mutex; - using Validation = typename Adaptor::Validation; - using Ledger = typename Adaptor::Ledger; - using ID = typename Ledger::ID; - using Seq = typename Ledger::Seq; - using NodeID = typename Validation::NodeID; - using NodeKey = typename Validation::NodeKey; + using Mutex = Adaptor::Mutex; + using Validation = Adaptor::Validation; + using Ledger = Adaptor::Ledger; + using ID = Ledger::ID; + using Seq = Ledger::Seq; + using NodeID = Validation::NodeID; + using NodeKey = Validation::NodeKey; using WrappedValidationType = std::decay_t>; diff --git a/src/xrpld/overlay/Slot.h b/src/xrpld/overlay/Slot.h index 0600265500..7490798787 100644 --- a/src/xrpld/overlay/Slot.h +++ b/src/xrpld/overlay/Slot.h @@ -82,7 +82,7 @@ class Slot final private: friend class Slots; using id_t = Peer::id_t; - using time_point = typename ClockType::time_point; + using time_point = ClockType::time_point; // a callback to report ignored squelches using ignored_squelch_callback = std::function; @@ -217,7 +217,7 @@ private: std::uint16_t reachedThreshold_{0}; // last time peers were selected, used to age the slot - typename ClockType::time_point lastSelected_; + ClockType::time_point lastSelected_; SlotState state_{SlotState::Counting}; // slot's state SquelchHandler const& handler_; // squelch/unsquelch handler @@ -483,7 +483,7 @@ Slot::notInState(PeerState state) const } template -std::set +std::set Slot::getSelected() const { std::set r; @@ -496,7 +496,7 @@ Slot::getSelected() const } template -std::unordered_map> +std::unordered_map> Slot::getPeers() const { using namespace std::chrono; @@ -526,8 +526,8 @@ Slot::getPeers() const template class Slots final { - using time_point = typename ClockType::time_point; - using id_t = typename Peer::id_t; + using time_point = ClockType::time_point; + using id_t = Peer::id_t; using messages = beast::aged_unordered_map< uint256, std::unordered_set, @@ -600,7 +600,7 @@ public: PublicKey const& validator, id_t id, protocol::MessageType type, - typename Slot::ignored_squelch_callback callback); + Slot::ignored_squelch_callback callback); /** Check if peers stopped relaying messages * and if slots stopped receiving messages from the validator. @@ -651,9 +651,8 @@ public: /** Get peers info. Return map of peer's state, count, and squelch * expiration milliseconds. */ - std:: - unordered_map> - getPeers(PublicKey const& validator) + std::unordered_map> + getPeers(PublicKey const& validator) { auto const& it = slots_.find(validator); if (it != slots_.end()) @@ -742,7 +741,7 @@ Slots::updateSlotAndSquelch( PublicKey const& validator, id_t id, protocol::MessageType type, - typename Slot::ignored_squelch_callback callback) + Slot::ignored_squelch_callback callback) { if (!addPeerMessage(key, id)) return; diff --git a/src/xrpld/overlay/Squelch.h b/src/xrpld/overlay/Squelch.h index b509f293c2..96d8c26f1d 100644 --- a/src/xrpld/overlay/Squelch.h +++ b/src/xrpld/overlay/Squelch.h @@ -14,7 +14,7 @@ namespace xrpl::reduce_relay { template class Squelch { - using time_point = typename ClockType::time_point; + using time_point = ClockType::time_point; public: explicit Squelch(beast::Journal journal) : journal_(journal) diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 26d7e0a832..28fb6b33a4 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -293,7 +293,7 @@ public: /** Send a set of PeerFinder endpoints as a protocol message. */ template < class FwdIt, - class = typename std::enable_if_t< + class = std::enable_if_t< std::is_same_v::value_type, PeerFinder::Endpoint>>> void sendEndpoints(FwdIt first, FwdIt last); diff --git a/src/xrpld/overlay/detail/ZeroCopyStream.h b/src/xrpld/overlay/detail/ZeroCopyStream.h index d8d311105d..f77f266321 100644 --- a/src/xrpld/overlay/detail/ZeroCopyStream.h +++ b/src/xrpld/overlay/detail/ZeroCopyStream.h @@ -17,7 +17,7 @@ template class ZeroCopyInputStream : public ::google::protobuf::io::ZeroCopyInputStream { private: - using iterator = typename Buffers::const_iterator; + using iterator = Buffers::const_iterator; using const_buffer = boost::asio::const_buffer; google::protobuf::int64 count_ = 0; @@ -110,8 +110,8 @@ template class ZeroCopyOutputStream : public ::google::protobuf::io::ZeroCopyOutputStream { private: - using buffers_type = typename Streambuf::mutable_buffers_type; - using iterator = typename buffers_type::const_iterator; + using buffers_type = Streambuf::mutable_buffers_type; + using iterator = buffers_type::const_iterator; using mutable_buffer = boost::asio::mutable_buffer; Streambuf& streambuf_; diff --git a/src/xrpld/peerfinder/detail/Checker.h b/src/xrpld/peerfinder/detail/Checker.h index 8ab084830c..2f324bf8b6 100644 --- a/src/xrpld/peerfinder/detail/Checker.h +++ b/src/xrpld/peerfinder/detail/Checker.h @@ -34,8 +34,8 @@ private: template struct AsyncOp : BasicAsyncOp { - using socket_type = typename Protocol::socket; - using endpoint_type = typename Protocol::endpoint; + using socket_type = Protocol::socket; + using endpoint_type = Protocol::endpoint; Checker& checker; socket_type socket; @@ -57,8 +57,8 @@ private: //-------------------------------------------------------------------------- - using list_type = typename boost::intrusive:: - make_list>::type; + using list_type = + boost::intrusive::make_list>::type; std::mutex mutex_; std::condition_variable cond_; diff --git a/src/xrpld/peerfinder/detail/Livecache.h b/src/xrpld/peerfinder/detail/Livecache.h index 84efcb7bd1..c5f04be90a 100644 --- a/src/xrpld/peerfinder/detail/Livecache.h +++ b/src/xrpld/peerfinder/detail/Livecache.h @@ -65,12 +65,12 @@ public: }; public: - using iterator = boost::transform_iterator; + using iterator = boost::transform_iterator; using const_iterator = iterator; using reverse_iterator = - boost::transform_iterator; + boost::transform_iterator; using const_reverse_iterator = reverse_iterator; @@ -132,7 +132,7 @@ public: } private: - explicit Hop(typename beast::MaybeConst::type& list) : list_(list) + explicit Hop(beast::MaybeConst::type& list) : list_(list) { } @@ -145,7 +145,7 @@ protected: // Work-around to call Hop's private constructor from Livecache template static Hop - makeHop(typename beast::MaybeConst::type& list) + makeHop(beast::MaybeConst::type& list) { return Hop(list); } @@ -208,30 +208,29 @@ public: template struct Transform { - using first_argument = typename lists_type::value_type; + using first_argument = lists_type::value_type; using result_type = Hop; explicit Transform() = default; Hop - operator()(typename beast::MaybeConst::type& - list) const + operator()(beast::MaybeConst::type& list) const { return makeHop(list); } }; public: - using iterator = boost::transform_iterator, typename lists_type::iterator>; + using iterator = boost::transform_iterator, lists_type::iterator>; using const_iterator = - boost::transform_iterator, typename lists_type::const_iterator>; + boost::transform_iterator, lists_type::const_iterator>; using reverse_iterator = - boost::transform_iterator, typename lists_type::reverse_iterator>; + boost::transform_iterator, lists_type::reverse_iterator>; using const_reverse_iterator = - boost::transform_iterator, typename lists_type::const_reverse_iterator>; + boost::transform_iterator, lists_type::const_reverse_iterator>; iterator begin() @@ -338,7 +337,7 @@ public: } /** Returns the number of entries in the cache. */ - typename cache_type::size_type + cache_type::size_type size() const { return cache_.size(); diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 815858cf00..55cce506ba 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -980,7 +980,7 @@ public: /** Adds eligible Fixed addresses for outbound attempts. */ template void - getFixed(std::size_t needed, Container& c, typename ConnectHandouts::Squelches& squelches) + getFixed(std::size_t needed, Container& c, ConnectHandouts::Squelches& squelches) { auto const now(clock.now()); for (auto iter = fixed_.begin(); needed && iter != fixed_.end(); ++iter) diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index f405ffe4de..123b9fc7a5 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -1589,7 +1589,7 @@ struct RPCCallImp jvResult["result"] = jvReply; - (callbackFuncP)(jvResult); + callbackFuncP(jvResult); } return false; diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp index c783319049..4b5f1821e3 100644 --- a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp @@ -39,7 +39,7 @@ estimateEntropy(std::string const& input) { (void)_; auto x = f / input.length(); - se += (x)*log2(x); + se += x * log2(x); } // We multiply it by the length, to get an estimate of diff --git a/src/xrpld/rpc/json_body.h b/src/xrpld/rpc/json_body.h index 4520a8bf39..5c4c56cef8 100644 --- a/src/xrpld/rpc/json_body.h +++ b/src/xrpld/rpc/json_body.h @@ -22,7 +22,7 @@ struct JsonBody dynamic_buffer_type buffer_; public: - using const_buffers_type = typename dynamic_buffer_type::const_buffers_type; + using const_buffers_type = dynamic_buffer_type::const_buffers_type; using is_deferred = std::false_type; From 556d62a0deee94cc55b2f9cfb29e29dd7926aa1c Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Wed, 24 Jun 2026 16:53:46 -0700 Subject: [PATCH 009/100] build: Align xrpld RPM packaging with DEB package (#7529) --- .github/workflows/reusable-package.yml | 20 +--- cmake/XrplPackaging.cmake | 1 - cspell.config.yaml | 1 + package/README.md | 159 ++++++++++++++++--------- package/build_pkg.sh | 148 ++++++++++++----------- package/rpm/xrpld.spec | 22 +++- package/shared/50-xrpld.preset | 2 - 7 files changed, 203 insertions(+), 150 deletions(-) delete mode 100644 package/shared/50-xrpld.preset diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index eed4bfc4a3..249e807592 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -39,23 +39,8 @@ jobs: working-directory: .github/scripts/strategy-matrix run: ./generate.py --packaging >>"${GITHUB_OUTPUT}" - generate-version: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - sparse-checkout: | - .github/actions/generate-version - src/libxrpl/protocol/BuildInfo.cpp - - name: Generate version - id: version - uses: ./.github/actions/generate-version - package: - needs: [generate-matrix, generate-version] + needs: [generate-matrix] if: ${{ github.event.repository.visibility == 'public' }} strategy: fail-fast: false @@ -82,14 +67,13 @@ jobs: - name: Build package env: - PKG_VERSION: ${{ needs.generate-version.outputs.version }} PKG_RELEASE: ${{ inputs.pkg_release }} run: ./package/build_pkg.sh - name: Upload package artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ matrix.artifact_name }}-pkg-${{ needs.generate-version.outputs.version }} + name: ${{ matrix.artifact_name }}-pkg path: | ${{ env.BUILD_DIR }}/debbuild/*.deb ${{ env.BUILD_DIR }}/debbuild/*.ddeb diff --git a/cmake/XrplPackaging.cmake b/cmake/XrplPackaging.cmake index fe885c200c..8e3861925d 100644 --- a/cmake/XrplPackaging.cmake +++ b/cmake/XrplPackaging.cmake @@ -28,7 +28,6 @@ endif() set(package_env SRC_DIR=${CMAKE_SOURCE_DIR} BUILD_DIR=${CMAKE_BINARY_DIR} - PKG_VERSION=${xrpld_version} PKG_RELEASE=${pkg_release} ) diff --git a/cspell.config.yaml b/cspell.config.yaml index 0d38c4be7b..8273df6c98 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -301,6 +301,7 @@ words: - txs - ubsan - UBSAN + - ufdio - umant - unacquired - unambiguity diff --git a/package/README.md b/package/README.md index 63c2ab88fc..4b78106c4c 100644 --- a/package/README.md +++ b/package/README.md @@ -6,10 +6,10 @@ This directory contains all files needed to build RPM and Debian packages for `x ``` package/ - build_pkg.sh Staging and build script (called by CMake targets and CI) + build_pkg.sh Staging and build script (called by the CMake `package` target and CI) rpm/ - xrpld.spec RPM spec (xrpld_version/pkg_release passed via rpmbuild --define) - debian/ Debian control files (control, rules, install, links, conffiles, ...) + xrpld.spec RPM spec + debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, source/format) shared/ xrpld.service systemd unit file (used by both RPM and DEB) xrpld.sysusers sysusers.d config (used by both RPM and DEB) @@ -21,21 +21,19 @@ package/ Packaging targets and their container images are declared in [`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json) -inside `package_configs` configurations. Today only -`linux/amd64` is emitted. The package format -(deb or rpm) is inferred at build time from the container's package manager -(`apt-get` -> deb, `dnf`/`yum` -> rpm). The image tag is composed as -`ghcr.io/xrplf/xrpld/packaging-:sha-` — -the same scheme used by `reusable-build-test.yml`. Bump `image_sha` in -`linux.json` and both CI and local builds pick up the new image with no -workflow edits. +under `package_configs`, one entry per distro. Today only `linux/amd64` is +emitted. Each entry pins its full container image in an `image` field; to move +to a new image, edit that field and both CI and local builds pick it up. The +package format (deb or rpm) is inferred at build time from the container's +package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). -| Package type | Image (derived from `linux.json`) | Tool required | -| ------------ | ---------------------------------------------------- | --------------------------------------------------------------- | -| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-` | `rpmbuild` | -| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-` | `dpkg-buildpackage`, `debhelper (>= 13)`, `dh-sequence-systemd` | +| Package type | Image (`package_configs.[].image` in `linux.json`) | Tools required | +| ------------ | ---------------------------------------------------------- | --------------------------------------------------- | +| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-` | `rpmbuild` | +| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-` | `dpkg-buildpackage`, debhelper with compat level 13 | -To print the exact image tags for the current `linux.json`: +To print the full packaging matrix (artifact names and images) for the current +`linux.json`: ```bash ./.github/scripts/strategy-matrix/generate.py --packaging @@ -46,12 +44,13 @@ To print the exact image tags for the current `linux.json`: ### Via CI Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call -`reusable-strategy-matrix.yml` with `mode: packaging` to generate the matrix of -`{artifact_name, os}` entries, then fan out to -`reusable-package.yml` per entry. That workflow downloads the pre-built `xrpld` -binary artifact, detects the package format from the container, and calls -`build_pkg.sh` directly — no CMake configure or build step is needed inside -the packaging job. +`reusable-package.yml`. That workflow generates its own packaging matrix from +`package_configs` in `linux.json` (via `generate.py --packaging`) and fans out +one job per distro. Each job downloads the pre-built `xrpld` binary artifact and +runs in that distro's container, so the package format follows from the +container's package manager. The packaging script derives the package version +from the downloaded binary's `xrpld --version` output; no CMake configure or +build step is needed inside the packaging job. ### Locally (mirrors CI) @@ -60,22 +59,19 @@ inside the same container CI uses. The image tag is derived from `linux.json` so you don't need to hardcode a SHA. ```bash -# From the repo root. Pick any image flagged with `"package": true` in -# linux.json; the package format is inferred from the container's package -# manager. Example for the rpm-producing image: -IMAGE=$(jq -r ' - .os | map(select(.package == true))[0] | - "ghcr.io/xrplf/ci/\(.distro_name)-\(.distro_version):\(.compiler_name)-\(.compiler_version)-sha-\(.image_sha)" -' .github/scripts/strategy-matrix/linux.json) +# From the repo root. Each distro's container image is the `image` field of its +# package_configs entry in linux.json; the package format is inferred from the +# container's package manager. Example for the rpm-producing image (use +# .package_configs.debian[0].image for the deb image): +IMAGE=$(jq -r '.package_configs.rhel[0].image' .github/scripts/strategy-matrix/linux.json) -VERSION=2.4.0-local PKG_RELEASE=1 docker run --rm \ -v "$(pwd):/src" \ -w /src \ - "$IMAGE" \ - ./package/build_pkg.sh --pkg-version "$VERSION" --pkg-release "$PKG_RELEASE" + "${IMAGE}" \ + ./package/build_pkg.sh --pkg-release "${PKG_RELEASE}" # Output: # build/debbuild/*.deb (DEB + dbgsym .ddeb) @@ -91,41 +87,73 @@ needed, but the host toolchain replaces the pinned CI image: ```bash cmake \ -Dxrpld=ON \ - -Dxrpld_version=2.4.0-local \ + -Dpkg_release=1 \ -Dtests=OFF \ .. cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL ``` -The `cmake/XrplPackaging.cmake` module defines the target only if at least one -of `rpmbuild` / `dpkg-buildpackage` is present; `build_pkg.sh` then infers the -package format from the host's package manager. The packaging script installs -to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of +The `cmake/XrplPackaging.cmake` module defines the `package` target only if at +least one of `rpmbuild` / `dpkg-buildpackage` is present; `build_pkg.sh` then +infers the package format from the host's package manager. The packaging script +installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of `CMAKE_INSTALL_PREFIX`. +The package version is not a CMake input on this path: `build_pkg.sh` derives it +from the just-built `xrpld` binary's `xrpld --version` output. The package +release defaults to 1 and is overridable with `-Dpkg_release=N`. + ## How `build_pkg.sh` works -`build_pkg.sh` accepts long-form flags, each of which can also be set via an -environment variable. Flags override env vars; env vars override the built-in -defaults. Run `./package/build_pkg.sh --help` for the same table: +`build_pkg.sh` derives the `xrpld` software version from +`${BUILD_DIR}/xrpld --version` in both package formats. -| Flag | Env var | Default | Purpose | -| -------------------------- | ------------------- | ----------------------------- | ----------------------------------- | -| `--src-dir DIR` | `SRC_DIR` | `$PWD` | repo root | -| `--build-dir DIR` | `BUILD_DIR` | `$PWD/build` | directory holding pre-built `xrpld` | -| `--pkg-version STR` | `PKG_VERSION` | parsed from `xrpld --version` | version string, e.g. `3.2.0-b1` | -| `--pkg-release N` | `PKG_RELEASE` | `1` | package release number | -| `--source-date-epoch SECS` | `SOURCE_DATE_EPOCH` | latest git commit ctime | reproducibility timestamp | +The binary's version is already SemVer-validated by `BuildInfo`. +`build_pkg.sh` converts pre-release versions such as `3.2.0-b1` or +`3.2.0-rc1` from `-` to `~` for package metadata so pre-releases sort before +the final release. If that normalized package version still contains `-`, +packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as +the upstream/revision separator. + +`pkg_version` is the normalized package metadata version derived inside +`build_pkg.sh` from the binary-reported `xrpld` version (`-` pre-release +separator converted to `~`). It is not a separate user input. + +`PKG_RELEASE` is a different value: the package release iteration for that +`xrpld` version. RPM receives the normalized `pkg_version` and `PKG_RELEASE` as +the `pkg_version` and `pkg_release` macros for its `Version` and `Release` +values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in +`debian/changelog`. + +With `PKG_RELEASE=1`, the package metadata becomes: + +| Input version | RPM version/release | Debian version | +| ------------------ | ---------------------------- | -------------------- | +| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` | +| `3.2.0-b0+abc1234` | `3.2.0~b0+abc1234-1%{?dist}` | `3.2.0~b0+abc1234-1` | +| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` | +| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` | + +The Debian changelog entry carries the repository component: final releases use +`stable`, `b0` builds, including `b0+metadata`, use `develop`, and `bN`/`rcN` +pre-releases use `unstable`. +Build metadata on a final release, such as `3.2.0+abc123`, is rejected. + +The RPM path intentionally uses `~` in `Version`, matching the Debian +pre-release ordering convention, so RPM filenames/NVRs begin with forms like +`xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding +pre-releases with an older `0..` RPM `Release` value. The package format (`deb` or `rpm`) is inferred from the host's package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those fail early. Flags are for explicit invocation; environment variables are intended for -CMake/systemd/CI integration. The CI workflow and the CMake `package` target -both invoke `build_pkg.sh` with no flags, configuring it entirely via env -(see `cmake/XrplPackaging.cmake`). +CMake/CI integration. The CI workflow and the CMake `package` target both invoke +`build_pkg.sh` with no flags; CMake supplies `SRC_DIR`, `BUILD_DIR`, and +`PKG_RELEASE` via env, while CI supplies `BUILD_DIR` and `PKG_RELEASE` via env +and lets the script use defaults for the rest. It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls `stage_common()` to copy the binary, config files, and shared support files @@ -134,18 +162,32 @@ into the staging area, and invokes the platform build tool. ### RPM 1. Creates the standard `rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}` tree inside the build directory. -2. Copies `xrpld.spec` and all source files (binary, configs, service files) into `SOURCES/`. -3. Runs `rpmbuild -bb --define "xrpld_version ..." --define "pkg_release ..."`. The spec uses manual `install` commands to place files. +2. Copies `xrpld.spec` and all shared source files (binary, configs, service files) into `SOURCES/`. +3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the + `pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro. + The spec uses manual `install` commands to place files, disables `dwz`, and + writes uncompressed RPM payloads while generating debuginfo packages. 4. Output: `rpmbuild/RPMS/x86_64/xrpld-*.rpm` +The uncompressed RPM payload setting is intentionally unconditional for +generated RPMs. It trades larger RPM artifacts for much shorter package +build/validation time, which keeps RPM package validation in the same rough time +class as Debian package validation. + +RPM upgrades intentionally do not restart a running `xrpld` service. The spec +uses `%systemd_postun`, matching Debian's `dh_installsystemd +--no-stop-on-upgrade` behavior; operators pick up the new binary on the next +service restart. + ### DEB 1. Creates a staging source tree at `debbuild/source/` inside the build directory. 2. Stages the binary, configs, `README.md`, and `LICENSE.md`. 3. Copies `package/debian/` control files into `debbuild/source/debian/`. 4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically. -5. Generates a minimal `debian/changelog` (pre-release versions use `~` instead of `-`). -6. Runs `dpkg-buildpackage -b --no-sign`. `debian/rules` uses manual `install` commands. +5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`, + where `pkg_version` is derived from the binary-reported `xrpld` version. +6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands. 7. Output: `debbuild/*.deb` and `debbuild/*.ddeb` (dbgsym package) ## Post-build verification @@ -161,11 +203,14 @@ rpm -qlp rpmbuild/RPMS/x86_64/*.rpm ## Reproducibility -The following environment variables improve build reproducibility. They are not -set automatically by `build_pkg.sh`; set them manually if needed: +`build_pkg.sh` already defaults `SOURCE_DATE_EPOCH` to the latest git commit +time, or the current time outside a git tree, and exports it (override with +`--source-date-epoch` / `SOURCE_DATE_EPOCH`); the RPM spec clamps file +modification times to it via `%build_mtime_policy`. The remaining variables +below further improve reproducibility but are _not_ set by the script — export +them yourself if needed: ```bash -export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) export TZ=UTC export LC_ALL=C.UTF-8 export GZIP=-n diff --git a/package/build_pkg.sh b/package/build_pkg.sh index e2ec8fee3d..3684fc096a 100755 --- a/package/build_pkg.sh +++ b/package/build_pkg.sh @@ -3,20 +3,18 @@ set -euo pipefail # Build an RPM or Debian package from a pre-built xrpld binary. # -# Flags override env vars; env vars override defaults. Env vars are intended -# for CMake/systemd/CI integration; flags are for explicit invocation. +# Flags override env vars; env vars override defaults. usage() { cat <<'EOF' Usage: build_pkg.sh [options] Options (each can also be set via the env var shown): - --src-dir DIR repo root [SRC_DIR; default: $PWD] - --build-dir DIR directory holding xrpld [BUILD_DIR; default: $PWD/build] - --pkg-version STR version, e.g. 3.2.0-b1 [PKG_VERSION; default: parsed from xrpld --version] - --pkg-release N package release number [PKG_RELEASE; default: 1] - --source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; default: latest git commit ctime] - -h, --help show this help and exit + --src-dir DIR repo root [SRC_DIR; default: ${PWD}] + --build-dir DIR directory holding xrpld [BUILD_DIR; default: ${PWD}/build] + --pkg-release N package release iteration [PKG_RELEASE; default: 1] + --source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; latest git ctime; fallback: current time] + -h, --help show this help and exit EOF } @@ -30,8 +28,7 @@ need_arg() { # Seed from env. CLI parsing below overrides these directly. SRC_DIR="${SRC_DIR:-}" BUILD_DIR="${BUILD_DIR:-}" -PKG_VERSION="${PKG_VERSION:-}" -PKG_RELEASE="${PKG_RELEASE:-}" +PKG_RELEASE="${PKG_RELEASE:-1}" SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-}" while [[ $# -gt 0 ]]; do @@ -46,11 +43,6 @@ while [[ $# -gt 0 ]]; do BUILD_DIR="$2" shift 2 ;; - --pkg-version) - need_arg "$@" - PKG_VERSION="$2" - shift 2 - ;; --pkg-release) need_arg "$@" PKG_RELEASE="$2" @@ -74,19 +66,61 @@ while [[ $# -gt 0 ]]; do done SRC_DIR="$(cd "${SRC_DIR:-${PWD}}" && pwd)" -BUILD_DIR="$(cd "${BUILD_DIR:-${PWD}/build}" && pwd)" -PKG_RELEASE="${PKG_RELEASE:-1}" - -if [[ -z "${PKG_VERSION}" ]]; then - PKG_VERSION="$("${BUILD_DIR}/xrpld" --version | awk 'NR==1 {print $3; exit}')" +BUILD_DIR="${BUILD_DIR:-${PWD}/build}" +if [[ ! -d "${BUILD_DIR}" ]]; then + echo "build_pkg.sh: build directory not found: ${BUILD_DIR}" >&2 + echo "Build xrpld before packaging, or set BUILD_DIR to the directory containing xrpld." >&2 + exit 1 fi +BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)" -if [[ -z "${PKG_VERSION}" ]]; then - echo "PKG_VERSION is empty (not provided and could not be derived)." >&2 +xrpld_binary="${BUILD_DIR}/xrpld" +if [[ ! -x "${xrpld_binary}" ]]; then + echo "build_pkg.sh: expected executable xrpld binary at ${xrpld_binary}." >&2 + echo "Build xrpld before packaging, or set BUILD_DIR to the directory containing xrpld." >&2 exit 1 fi -VERSION="${PKG_VERSION}" +xrpld_version="$("${xrpld_binary}" --version | awk 'NR == 1 { print $3 }')" + +if [[ -z "${xrpld_version}" ]]; then + echo "build_pkg.sh: unable to derive xrpld version from ${xrpld_binary} --version." >&2 + exit 1 +fi + +# The version as the package formats consume it: identical to xrpld_version +# except a pre-release uses '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before +# the final 3.2.0; a no-op for a final release. Lowercase = derived internally, +# not an input (cf. pkg_type). +pkg_version="${xrpld_version}" +pre_release="" +if [[ "${xrpld_version}" == *-* ]]; then + pre_release="${xrpld_version#*-}" + pkg_version="${xrpld_version%%-*}~${pre_release}" +fi + +# BuildInfo already SemVer-validates the binary's version. Packaging adds one +# narrower constraint: after pre-release normalization, the package version must +# not contain '-' because RPM forbids it in Version and Debian uses it as the +# upstream/revision separator. +if [[ "${pkg_version}" == *-* ]]; then + echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2 + echo "Package version '${pkg_version}' cannot contain '-'." >&2 + echo "Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2." >&2 + exit 1 +fi + +if [[ -z "${pre_release}" && "${xrpld_version}" == *+* ]]; then + echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2 + echo "Build metadata is only supported on bN/rcN pre-releases." >&2 + exit 1 +fi + +if [[ -n "${pre_release}" && ! "${pre_release}" =~ ^(b0|b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then + echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2 + echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2 + exit 1 +fi if command -v apt-get >/dev/null 2>&1; then pkg_type=deb @@ -98,32 +132,15 @@ else fi if [[ -z "${SOURCE_DATE_EPOCH}" ]]; then - if git -C "$SRC_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - SOURCE_DATE_EPOCH="$(git -C "$SRC_DIR" log -1 --format=%ct)" + if git -C "${SRC_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + SOURCE_DATE_EPOCH="$(git -C "${SRC_DIR}" log -1 --format=%ct)" else SOURCE_DATE_EPOCH="$(date +%s)" fi fi export SOURCE_DATE_EPOCH -CHANGELOG_DATE="$(date -u -R -d "@$SOURCE_DATE_EPOCH")" - -# Split VERSION at the first '-' into base and optional pre-release suffix. -# Examples: "3.2.0" -> ("3.2.0", ""); "3.2.0-b1" -> ("3.2.0", "b1"). -VER_BASE="${VERSION%%-*}" -VER_SUFFIX="${VERSION#*-}" -[[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX="" - -# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). Neither an -# RPM Version nor a Debian upstream version may contain '-' (it's the NVR / -# version-revision separator), and the convention here is single-token -# suffixes like b1 or rc2. Fail early with a clear message rather than letting -# the package tooling blow up or silently mangle dashes. -if [[ "${VER_SUFFIX}" == *-* ]]; then - echo "build_pkg.sh: multi-segment pre-release in VERSION='${VERSION}' (suffix '${VER_SUFFIX}')." >&2 - echo "Use single-token suffixes like 3.2.0-b1 or 3.2.0-rc2." >&2 - exit 1 -fi +CHANGELOG_DATE="$(date -u -R -d "@${SOURCE_DATE_EPOCH}")" SHARED="${SRC_DIR}/package/shared" DEBIAN_DIR="${SRC_DIR}/package/debian" @@ -143,7 +160,6 @@ stage_common() { cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers" cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles" cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate" - cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset" } build_rpm() { @@ -154,18 +170,11 @@ build_rpm() { cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec" stage_common "${topdir}/SOURCES" - # Pre-releases use the modern rpm '~' convention (rpm >= 4.10): the suffix - # goes in Version (e.g. 3.2.0~b1), which rpmvercmp sorts *before* the final - # 3.2.0 — identical semantics to Debian's '~'. Release is just the package - # release number. This replaces the older "0.." Release - # hack and keeps the RPM and DEB version strings symmetric. - local rpm_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}" - set -x rpmbuild -bb \ --define "_topdir ${topdir}" \ - --define "xrpld_version ${rpm_version}" \ - --define "xrpld_release ${PKG_RELEASE}" \ + --define "pkg_version ${pkg_version}" \ + --define "pkg_release ${PKG_RELEASE}" \ "${topdir}/SPECS/xrpld.spec" } @@ -182,23 +191,26 @@ build_deb() { cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles" cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate" - # Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0. - local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}" - - # Derive release channel from the version suffix: - # (none) -> stable (tagged release) - # b0 -> develop (develop-branch build) - # b, rc -> unstable (pre-release) - local deb_distribution - case "${VER_SUFFIX}" in - "") deb_distribution="stable" ;; - b0) deb_distribution="develop" ;; - *) deb_distribution="unstable" ;; - esac + # Choose the Debian repository component for this package. + # 3.2.0 -> stable, *-b0[+metadata] -> develop, + # bN/rcN pre-releases -> unstable. + local deb_component + if [[ -z "${pre_release}" ]]; then + deb_component="stable" + elif [[ "${pre_release}" =~ ^b0(\+.*)?$ ]]; then + deb_component="develop" + elif [[ "${pre_release}" =~ ^(b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then + deb_component="unstable" + else + echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2 + echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2 + exit 1 + fi + # Debian version is [~
]-.
     cat >"${staging}/debian/changelog" <  ${CHANGELOG_DATE}
 EOF
diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec
index 5595fd0d8d..61c2d61ec6 100644
--- a/package/rpm/xrpld.spec
+++ b/package/rpm/xrpld.spec
@@ -1,6 +1,14 @@
+%if "%{?pkg_version}" == ""
+%{error:pkg_version must be defined}
+%endif
+
+%if "%{?pkg_release}" == ""
+%{error:pkg_release must be defined}
+%endif
+
 Name:     xrpld
-Version:  %{xrpld_version}
-Release:  %{xrpld_release}%{?dist}
+Version:  %{pkg_version}
+Release:  %{pkg_release}%{?dist}
 Summary:  XRP Ledger daemon
 
 License:  ISC
@@ -11,6 +19,9 @@ BuildRequires: systemd-rpm-macros
 
 %undefine _debugsource_packages
 %debug_package
+# Intentionally trade larger RPM artifacts for faster package validation.
+%global _binary_payload w.ufdio
+%global _find_debuginfo_dwz_opts %{nil}
 
 %build_mtime_policy clamp_to_source_date_epoch
 
@@ -37,7 +48,10 @@ install -Dm0644 %{_sourcedir}/validators.txt       %{buildroot}%{_sysconfdir}/%{
 install -Dm0644 %{_sourcedir}/xrpld.service        %{buildroot}%{_unitdir}/xrpld.service
 install -Dm0644 %{_sourcedir}/xrpld.sysusers       %{buildroot}%{_sysusersdir}/xrpld.conf
 install -Dm0644 %{_sourcedir}/xrpld.tmpfiles       %{buildroot}%{_tmpfilesdir}/xrpld.conf
-install -Dm0644 %{_sourcedir}/50-xrpld.preset      %{buildroot}%{_presetdir}/50-xrpld.preset
+install -Dm0644 /dev/null %{buildroot}%{_presetdir}/50-xrpld.preset
+cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
+enable xrpld.service
+EOF
 
 # Logrotate config
 install -Dm0644 %{_sourcedir}/xrpld.logrotate      %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
@@ -62,7 +76,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
 %systemd_preun xrpld.service
 
 %postun
-%systemd_postun_with_restart xrpld.service
+%systemd_postun xrpld.service
 
 %files
 %license %{_docdir}/%{name}/LICENSE.md
diff --git a/package/shared/50-xrpld.preset b/package/shared/50-xrpld.preset
deleted file mode 100644
index bfbcd56577..0000000000
--- a/package/shared/50-xrpld.preset
+++ /dev/null
@@ -1,2 +0,0 @@
-# /usr/lib/systemd/system-preset/50-xrpld.preset
-enable xrpld.service

From 3097c157b6ca13d368eef8b03d26fa027b277a4c Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 25 Jun 2026 13:40:06 +0100
Subject: [PATCH 010/100] build: Switch to a new conan XRPLF remote (#7622)

---
 .github/actions/setup-conan/action.yml       |  2 +-
 .github/workflows/on-pr.yml                  |  4 +-
 .github/workflows/on-tag.yml                 |  4 +-
 .github/workflows/on-trigger.yml             |  4 +-
 .github/workflows/reusable-upload-recipe.yml | 20 ++----
 .github/workflows/upload-conan-deps.yml      |  6 +-
 BUILD.md                                     |  2 +-
 conan.lock                                   | 64 ++++++++++----------
 conan/lockfile/regenerate.sh                 |  2 +-
 conanfile.py                                 |  4 +-
 docs/build/advanced_conan.md                 |  2 +-
 11 files changed, 54 insertions(+), 60 deletions(-)

diff --git a/.github/actions/setup-conan/action.yml b/.github/actions/setup-conan/action.yml
index 0dd22f0d92..e8a548cfce 100644
--- a/.github/actions/setup-conan/action.yml
+++ b/.github/actions/setup-conan/action.yml
@@ -9,7 +9,7 @@ inputs:
   remote_url:
     description: "The URL of the Conan endpoint to use."
     required: false
-    default: https://conan.ripplex.io
+    default: https://conan.xrplf.org/repository/conan/
 
 runs:
   using: composite
diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml
index 0c9eeda712..2ad0641863 100644
--- a/.github/workflows/on-pr.yml
+++ b/.github/workflows/on-pr.yml
@@ -154,8 +154,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
 
   notify-clio:
     needs: upload-recipe
diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml
index 42d5827cab..abedc13d69 100644
--- a/.github/workflows/on-tag.yml
+++ b/.github/workflows/on-tag.yml
@@ -20,8 +20,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
 
   build-test:
     if: ${{ github.repository == 'XRPLF/rippled' }}
diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml
index 063cdbff7f..5f018cb12c 100644
--- a/.github/workflows/on-trigger.yml
+++ b/.github/workflows/on-trigger.yml
@@ -98,8 +98,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
 
   package:
     needs: build-test
diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml
index a18f76796a..feeee0a621 100644
--- a/.github/workflows/reusable-upload-recipe.yml
+++ b/.github/workflows/reusable-upload-recipe.yml
@@ -14,7 +14,7 @@ on:
         description: "The URL of the Conan endpoint to use."
         required: false
         type: string
-        default: https://conan.ripplex.io
+        default: https://conan.xrplf.org/repository/conan/
 
     secrets:
       remote_username:
@@ -41,6 +41,10 @@ jobs:
   upload:
     runs-on: ubuntu-latest
     container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
+    env:
+      REMOTE_NAME: ${{ inputs.remote_name }}
+      CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
+      CONAN_PASSWORD_XRPLF: ${{ secrets.remote_password }}
     steps:
       - name: Checkout repository
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -56,15 +60,9 @@ jobs:
           remote_url: ${{ inputs.remote_url }}
 
       - name: Log into Conan remote
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
-          REMOTE_USERNAME: ${{ secrets.remote_username }}
-          REMOTE_PASSWORD: ${{ secrets.remote_password }}
-        run: conan remote login "${REMOTE_NAME}" "${REMOTE_USERNAME}" --password "${REMOTE_PASSWORD}"
+        run: conan remote login "${REMOTE_NAME}" "${CONAN_LOGIN_USERNAME_XRPLF}" --password "${CONAN_PASSWORD_XRPLF}"
 
       - name: Upload Conan recipe (version)
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=${{ steps.version.outputs.version }}
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.version.outputs.version }}
@@ -73,8 +71,6 @@ jobs:
       # 'develop' branch, see on-trigger.yml.
       - name: Upload Conan recipe (develop)
         if: ${{ github.event_name == 'push' }}
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=develop
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/develop
@@ -83,8 +79,6 @@ jobs:
       # one of the 'release' branches, see on-pr.yml.
       - name: Upload Conan recipe (rc)
         if: ${{ github.event_name == 'pull_request' }}
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=rc
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/rc
@@ -93,8 +87,6 @@ jobs:
       # release, see on-tag.yml.
       - name: Upload Conan recipe (release)
         if: ${{ startsWith(github.ref, 'refs/tags/') }}
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=release
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/release
diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml
index 5d3712cf9e..92b72cf6a9 100644
--- a/.github/workflows/upload-conan-deps.yml
+++ b/.github/workflows/upload-conan-deps.yml
@@ -34,7 +34,7 @@ on:
 
 env:
   CONAN_REMOTE_NAME: xrplf
-  CONAN_REMOTE_URL: https://conan.ripplex.io
+  CONAN_REMOTE_URL: https://conan.xrplf.org/repository/conan/
   NPROC_SUBTRACT: 2
 
 concurrency:
@@ -108,10 +108,12 @@ jobs:
 
       - name: Log into Conan remote
         if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
-        run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
+        run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.NEXUS_REMOTE_USERNAME }}" --password "${{ secrets.NEXUS_REMOTE_PASSWORD }}"
 
       - name: Upload Conan packages
         if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
         env:
           FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
+          CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+          CONAN_PASSWORD_XRPLF: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
         run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}
diff --git a/BUILD.md b/BUILD.md
index 2ac24f2c5d..847cd7bc1a 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -101,7 +101,7 @@ More information on customizing Conan can be found in the [Advanced Conan config
 Run the following command to add the `xrplf` remote, which hosts some of our dependencies:
 
 ```bash
-conan remote add --index 0 --force xrplf https://conan.ripplex.io
+conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
 ```
 
 ### Set Up Ccache
diff --git a/conan.lock b/conan.lock
index d80a6d0c57..ae45a900b6 100644
--- a/conan.lock
+++ b/conan.lock
@@ -1,43 +1,43 @@
 {
     "version": "0.5",
     "requires": [
-        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
-        "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
-        "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1778091117.311",
-        "soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
-        "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
-        "secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
-        "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
-        "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
-        "protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
-        "openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
-        "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
-        "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
-        "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
-        "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
-        "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848",
+        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
+        "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1743678659.187",
+        "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1776096494.149",
+        "soci/4.0.3#e726491a03468795453f7c83fc924a96%1751554127.172",
+        "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
+        "secp256k1/0.7.1#b1f450b7f78a36fff75bb6934a356f3a%1782338841.3729",
+        "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1759820024.194",
+        "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1772560729.95",
+        "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1778763145.334",
+        "openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
+        "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1774883011.384",
+        "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1775037240.923",
+        "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1774021608.288",
+        "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1722218217.276",
+        "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1776147552.838",
         "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
-        "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
-        "grpc/1.81.0#2fb144aeb47e7f35c6ebb0e5f35bed31%1781620605.685",
-        "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
-        "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
-        "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
-        "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837",
-        "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778091165.282",
-        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
+        "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1755784855.585",
+        "grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
+        "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
+        "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1754573467.979",
+        "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1766500685.317",
+        "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1762886692.465",
+        "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778050991.9",
+        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
     ],
     "build_requires": [
-        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
-        "strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
-        "protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
-        "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
+        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
+        "strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1751971032.423",
+        "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1778763145.334",
+        "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1745483323.489",
         "msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
-        "m4/1.4.19#4523e4347b55cd26ae918bd5770cab9a%1778062762.471",
-        "cmake/4.3.0#b939a42e98f593fb34d3a8c5cc860359%1774439249.183",
-        "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1774439233.447",
+        "m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259",
+        "cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1781521538.233",
+        "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1766594659.866",
         "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
         "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
-        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
+        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
     ],
     "python_requires": [],
     "overrides": {
@@ -57,7 +57,7 @@
             "boost/1.91.0"
         ],
         "lz4/[>=1.9.4 <2]": [
-            "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504"
+            "lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
         ]
     },
     "config_requires": []
diff --git a/conan/lockfile/regenerate.sh b/conan/lockfile/regenerate.sh
index 1aa47628f0..98ee6f7c99 100755
--- a/conan/lockfile/regenerate.sh
+++ b/conan/lockfile/regenerate.sh
@@ -14,7 +14,7 @@ export CONAN_HOME="$TEMP_DIR"
 # Ensure that the xrplf remote is the first to be consulted, so any recipes we
 # patched are used. We also add it there to not created huge diff when the
 # official Conan Center Index is updated.
-conan remote add --force --index 0 xrplf https://conan.ripplex.io
+conan remote add --force --index 0 xrplf https://conan.xrplf.org/repository/conan/
 
 # Delete any existing lockfile.
 rm -f conan.lock
diff --git a/conanfile.py b/conanfile.py
index 5b78dc22e3..2733d4fc9c 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -28,10 +28,10 @@ class Xrpl(ConanFile):
 
     requires = [
         "ed25519/2015.03",
-        "grpc/1.81.0",
+        "grpc/1.81.1",
         "libarchive/3.8.7",
         "nudb/2.0.9",
-        "openssl/3.6.2",
+        "openssl/3.6.3",
         "secp256k1/0.7.1",
         "soci/4.0.3",
         "zlib/1.3.2",
diff --git a/docs/build/advanced_conan.md b/docs/build/advanced_conan.md
index aae17e385a..26b88ef186 100644
--- a/docs/build/advanced_conan.md
+++ b/docs/build/advanced_conan.md
@@ -34,7 +34,7 @@ higher index than the default Conan Center remote, so it is consulted first. You
 can do this by running:
 
 ```bash
-conan remote add --index 0 --force xrplf https://conan.ripplex.io
+conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
 ```
 
 Alternatively, you can pull our recipes from the repository and export them locally:

From 07c64f07f02cedf9eeb185fc7c28e2d6711fc113 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 25 Jun 2026 15:47:55 +0100
Subject: [PATCH 011/100] chore: Revert "build: Switch to a new conan XRPLF
 remote (#7622)" (#7623)

---
 .github/actions/setup-conan/action.yml       |  2 +-
 .github/workflows/on-pr.yml                  |  4 +-
 .github/workflows/on-tag.yml                 |  4 +-
 .github/workflows/on-trigger.yml             |  4 +-
 .github/workflows/reusable-upload-recipe.yml | 20 ++++--
 .github/workflows/upload-conan-deps.yml      |  6 +-
 BUILD.md                                     |  2 +-
 conan.lock                                   | 64 ++++++++++----------
 conan/lockfile/regenerate.sh                 |  2 +-
 conanfile.py                                 |  4 +-
 docs/build/advanced_conan.md                 |  2 +-
 11 files changed, 60 insertions(+), 54 deletions(-)

diff --git a/.github/actions/setup-conan/action.yml b/.github/actions/setup-conan/action.yml
index e8a548cfce..0dd22f0d92 100644
--- a/.github/actions/setup-conan/action.yml
+++ b/.github/actions/setup-conan/action.yml
@@ -9,7 +9,7 @@ inputs:
   remote_url:
     description: "The URL of the Conan endpoint to use."
     required: false
-    default: https://conan.xrplf.org/repository/conan/
+    default: https://conan.ripplex.io
 
 runs:
   using: composite
diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml
index 2ad0641863..0c9eeda712 100644
--- a/.github/workflows/on-pr.yml
+++ b/.github/workflows/on-pr.yml
@@ -154,8 +154,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
 
   notify-clio:
     needs: upload-recipe
diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml
index abedc13d69..42d5827cab 100644
--- a/.github/workflows/on-tag.yml
+++ b/.github/workflows/on-tag.yml
@@ -20,8 +20,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
 
   build-test:
     if: ${{ github.repository == 'XRPLF/rippled' }}
diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml
index 5f018cb12c..063cdbff7f 100644
--- a/.github/workflows/on-trigger.yml
+++ b/.github/workflows/on-trigger.yml
@@ -98,8 +98,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
 
   package:
     needs: build-test
diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml
index feeee0a621..a18f76796a 100644
--- a/.github/workflows/reusable-upload-recipe.yml
+++ b/.github/workflows/reusable-upload-recipe.yml
@@ -14,7 +14,7 @@ on:
         description: "The URL of the Conan endpoint to use."
         required: false
         type: string
-        default: https://conan.xrplf.org/repository/conan/
+        default: https://conan.ripplex.io
 
     secrets:
       remote_username:
@@ -41,10 +41,6 @@ jobs:
   upload:
     runs-on: ubuntu-latest
     container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
-    env:
-      REMOTE_NAME: ${{ inputs.remote_name }}
-      CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
-      CONAN_PASSWORD_XRPLF: ${{ secrets.remote_password }}
     steps:
       - name: Checkout repository
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -60,9 +56,15 @@ jobs:
           remote_url: ${{ inputs.remote_url }}
 
       - name: Log into Conan remote
-        run: conan remote login "${REMOTE_NAME}" "${CONAN_LOGIN_USERNAME_XRPLF}" --password "${CONAN_PASSWORD_XRPLF}"
+        env:
+          REMOTE_NAME: ${{ inputs.remote_name }}
+          REMOTE_USERNAME: ${{ secrets.remote_username }}
+          REMOTE_PASSWORD: ${{ secrets.remote_password }}
+        run: conan remote login "${REMOTE_NAME}" "${REMOTE_USERNAME}" --password "${REMOTE_PASSWORD}"
 
       - name: Upload Conan recipe (version)
+        env:
+          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=${{ steps.version.outputs.version }}
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.version.outputs.version }}
@@ -71,6 +73,8 @@ jobs:
       # 'develop' branch, see on-trigger.yml.
       - name: Upload Conan recipe (develop)
         if: ${{ github.event_name == 'push' }}
+        env:
+          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=develop
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/develop
@@ -79,6 +83,8 @@ jobs:
       # one of the 'release' branches, see on-pr.yml.
       - name: Upload Conan recipe (rc)
         if: ${{ github.event_name == 'pull_request' }}
+        env:
+          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=rc
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/rc
@@ -87,6 +93,8 @@ jobs:
       # release, see on-tag.yml.
       - name: Upload Conan recipe (release)
         if: ${{ startsWith(github.ref, 'refs/tags/') }}
+        env:
+          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=release
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/release
diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml
index 92b72cf6a9..5d3712cf9e 100644
--- a/.github/workflows/upload-conan-deps.yml
+++ b/.github/workflows/upload-conan-deps.yml
@@ -34,7 +34,7 @@ on:
 
 env:
   CONAN_REMOTE_NAME: xrplf
-  CONAN_REMOTE_URL: https://conan.xrplf.org/repository/conan/
+  CONAN_REMOTE_URL: https://conan.ripplex.io
   NPROC_SUBTRACT: 2
 
 concurrency:
@@ -108,12 +108,10 @@ jobs:
 
       - name: Log into Conan remote
         if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
-        run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.NEXUS_REMOTE_USERNAME }}" --password "${{ secrets.NEXUS_REMOTE_PASSWORD }}"
+        run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
 
       - name: Upload Conan packages
         if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
         env:
           FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
-          CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.NEXUS_REMOTE_USERNAME }}
-          CONAN_PASSWORD_XRPLF: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
         run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}
diff --git a/BUILD.md b/BUILD.md
index 847cd7bc1a..2ac24f2c5d 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -101,7 +101,7 @@ More information on customizing Conan can be found in the [Advanced Conan config
 Run the following command to add the `xrplf` remote, which hosts some of our dependencies:
 
 ```bash
-conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
+conan remote add --index 0 --force xrplf https://conan.ripplex.io
 ```
 
 ### Set Up Ccache
diff --git a/conan.lock b/conan.lock
index ae45a900b6..d80a6d0c57 100644
--- a/conan.lock
+++ b/conan.lock
@@ -1,43 +1,43 @@
 {
     "version": "0.5",
     "requires": [
-        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
-        "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1743678659.187",
-        "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1776096494.149",
-        "soci/4.0.3#e726491a03468795453f7c83fc924a96%1751554127.172",
-        "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
-        "secp256k1/0.7.1#b1f450b7f78a36fff75bb6934a356f3a%1782338841.3729",
-        "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1759820024.194",
-        "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1772560729.95",
-        "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1778763145.334",
-        "openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
-        "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1774883011.384",
-        "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1775037240.923",
-        "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1774021608.288",
-        "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1722218217.276",
-        "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1776147552.838",
+        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
+        "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
+        "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1778091117.311",
+        "soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
+        "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
+        "secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
+        "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
+        "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
+        "protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
+        "openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
+        "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
+        "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
+        "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
+        "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
+        "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848",
         "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
-        "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1755784855.585",
-        "grpc/1.81.1#5217e6ef0544c42b46f4af35d5e7f649%1782307148.845616",
-        "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
-        "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1754573467.979",
-        "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1766500685.317",
-        "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1762886692.465",
-        "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778050991.9",
-        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
+        "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
+        "grpc/1.81.0#2fb144aeb47e7f35c6ebb0e5f35bed31%1781620605.685",
+        "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
+        "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
+        "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
+        "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837",
+        "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778091165.282",
+        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
     ],
     "build_requires": [
-        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
-        "strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1751971032.423",
-        "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1778763145.334",
-        "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1745483323.489",
+        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
+        "strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
+        "protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
+        "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
         "msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
-        "m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259",
-        "cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1781521538.233",
-        "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1766594659.866",
+        "m4/1.4.19#4523e4347b55cd26ae918bd5770cab9a%1778062762.471",
+        "cmake/4.3.0#b939a42e98f593fb34d3a8c5cc860359%1774439249.183",
+        "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1774439233.447",
         "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
         "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
-        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
+        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
     ],
     "python_requires": [],
     "overrides": {
@@ -57,7 +57,7 @@
             "boost/1.91.0"
         ],
         "lz4/[>=1.9.4 <2]": [
-            "lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
+            "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504"
         ]
     },
     "config_requires": []
diff --git a/conan/lockfile/regenerate.sh b/conan/lockfile/regenerate.sh
index 98ee6f7c99..1aa47628f0 100755
--- a/conan/lockfile/regenerate.sh
+++ b/conan/lockfile/regenerate.sh
@@ -14,7 +14,7 @@ export CONAN_HOME="$TEMP_DIR"
 # Ensure that the xrplf remote is the first to be consulted, so any recipes we
 # patched are used. We also add it there to not created huge diff when the
 # official Conan Center Index is updated.
-conan remote add --force --index 0 xrplf https://conan.xrplf.org/repository/conan/
+conan remote add --force --index 0 xrplf https://conan.ripplex.io
 
 # Delete any existing lockfile.
 rm -f conan.lock
diff --git a/conanfile.py b/conanfile.py
index 2733d4fc9c..5b78dc22e3 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -28,10 +28,10 @@ class Xrpl(ConanFile):
 
     requires = [
         "ed25519/2015.03",
-        "grpc/1.81.1",
+        "grpc/1.81.0",
         "libarchive/3.8.7",
         "nudb/2.0.9",
-        "openssl/3.6.3",
+        "openssl/3.6.2",
         "secp256k1/0.7.1",
         "soci/4.0.3",
         "zlib/1.3.2",
diff --git a/docs/build/advanced_conan.md b/docs/build/advanced_conan.md
index 26b88ef186..aae17e385a 100644
--- a/docs/build/advanced_conan.md
+++ b/docs/build/advanced_conan.md
@@ -34,7 +34,7 @@ higher index than the default Conan Center remote, so it is consulted first. You
 can do this by running:
 
 ```bash
-conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
+conan remote add --index 0 --force xrplf https://conan.ripplex.io
 ```
 
 Alternatively, you can pull our recipes from the repository and export them locally:

From 0711a7b493c63d261cb7fcb02b22c9de744d8ff7 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 25 Jun 2026 23:06:04 +0100
Subject: [PATCH 012/100] build: Switch to a new conan XRPLF remote, again
 (#7638)

---
 .github/actions/setup-conan/action.yml       |  2 +-
 .github/workflows/on-pr.yml                  |  4 +-
 .github/workflows/on-tag.yml                 |  4 +-
 .github/workflows/on-trigger.yml             |  4 +-
 .github/workflows/reusable-upload-recipe.yml | 20 ++----
 .github/workflows/upload-conan-deps.yml      |  6 +-
 BUILD.md                                     |  2 +-
 conan.lock                                   | 64 ++++++++++----------
 conan/lockfile/regenerate.sh                 |  2 +-
 conanfile.py                                 |  4 +-
 docs/build/advanced_conan.md                 |  2 +-
 11 files changed, 54 insertions(+), 60 deletions(-)

diff --git a/.github/actions/setup-conan/action.yml b/.github/actions/setup-conan/action.yml
index 0dd22f0d92..e8a548cfce 100644
--- a/.github/actions/setup-conan/action.yml
+++ b/.github/actions/setup-conan/action.yml
@@ -9,7 +9,7 @@ inputs:
   remote_url:
     description: "The URL of the Conan endpoint to use."
     required: false
-    default: https://conan.ripplex.io
+    default: https://conan.xrplf.org/repository/conan/
 
 runs:
   using: composite
diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml
index 0c9eeda712..2ad0641863 100644
--- a/.github/workflows/on-pr.yml
+++ b/.github/workflows/on-pr.yml
@@ -154,8 +154,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
 
   notify-clio:
     needs: upload-recipe
diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml
index 42d5827cab..abedc13d69 100644
--- a/.github/workflows/on-tag.yml
+++ b/.github/workflows/on-tag.yml
@@ -20,8 +20,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
 
   build-test:
     if: ${{ github.repository == 'XRPLF/rippled' }}
diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml
index 063cdbff7f..5f018cb12c 100644
--- a/.github/workflows/on-trigger.yml
+++ b/.github/workflows/on-trigger.yml
@@ -98,8 +98,8 @@ jobs:
     if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
     uses: ./.github/workflows/reusable-upload-recipe.yml
     secrets:
-      remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
-      remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
+      remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+      remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
 
   package:
     needs: build-test
diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml
index a18f76796a..feeee0a621 100644
--- a/.github/workflows/reusable-upload-recipe.yml
+++ b/.github/workflows/reusable-upload-recipe.yml
@@ -14,7 +14,7 @@ on:
         description: "The URL of the Conan endpoint to use."
         required: false
         type: string
-        default: https://conan.ripplex.io
+        default: https://conan.xrplf.org/repository/conan/
 
     secrets:
       remote_username:
@@ -41,6 +41,10 @@ jobs:
   upload:
     runs-on: ubuntu-latest
     container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-e29b523
+    env:
+      REMOTE_NAME: ${{ inputs.remote_name }}
+      CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
+      CONAN_PASSWORD_XRPLF: ${{ secrets.remote_password }}
     steps:
       - name: Checkout repository
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -56,15 +60,9 @@ jobs:
           remote_url: ${{ inputs.remote_url }}
 
       - name: Log into Conan remote
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
-          REMOTE_USERNAME: ${{ secrets.remote_username }}
-          REMOTE_PASSWORD: ${{ secrets.remote_password }}
-        run: conan remote login "${REMOTE_NAME}" "${REMOTE_USERNAME}" --password "${REMOTE_PASSWORD}"
+        run: conan remote login "${REMOTE_NAME}" "${CONAN_LOGIN_USERNAME_XRPLF}" --password "${CONAN_PASSWORD_XRPLF}"
 
       - name: Upload Conan recipe (version)
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=${{ steps.version.outputs.version }}
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.version.outputs.version }}
@@ -73,8 +71,6 @@ jobs:
       # 'develop' branch, see on-trigger.yml.
       - name: Upload Conan recipe (develop)
         if: ${{ github.event_name == 'push' }}
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=develop
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/develop
@@ -83,8 +79,6 @@ jobs:
       # one of the 'release' branches, see on-pr.yml.
       - name: Upload Conan recipe (rc)
         if: ${{ github.event_name == 'pull_request' }}
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=rc
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/rc
@@ -93,8 +87,6 @@ jobs:
       # release, see on-tag.yml.
       - name: Upload Conan recipe (release)
         if: ${{ startsWith(github.ref, 'refs/tags/') }}
-        env:
-          REMOTE_NAME: ${{ inputs.remote_name }}
         run: |
           conan export . --version=release
           conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/release
diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml
index 5d3712cf9e..92b72cf6a9 100644
--- a/.github/workflows/upload-conan-deps.yml
+++ b/.github/workflows/upload-conan-deps.yml
@@ -34,7 +34,7 @@ on:
 
 env:
   CONAN_REMOTE_NAME: xrplf
-  CONAN_REMOTE_URL: https://conan.ripplex.io
+  CONAN_REMOTE_URL: https://conan.xrplf.org/repository/conan/
   NPROC_SUBTRACT: 2
 
 concurrency:
@@ -108,10 +108,12 @@ jobs:
 
       - name: Log into Conan remote
         if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
-        run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
+        run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.NEXUS_REMOTE_USERNAME }}" --password "${{ secrets.NEXUS_REMOTE_PASSWORD }}"
 
       - name: Upload Conan packages
         if: ${{ github.repository == 'XRPLF/rippled' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
         env:
           FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
+          CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+          CONAN_PASSWORD_XRPLF: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
         run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}
diff --git a/BUILD.md b/BUILD.md
index 2ac24f2c5d..847cd7bc1a 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -101,7 +101,7 @@ More information on customizing Conan can be found in the [Advanced Conan config
 Run the following command to add the `xrplf` remote, which hosts some of our dependencies:
 
 ```bash
-conan remote add --index 0 --force xrplf https://conan.ripplex.io
+conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
 ```
 
 ### Set Up Ccache
diff --git a/conan.lock b/conan.lock
index d80a6d0c57..45dd145914 100644
--- a/conan.lock
+++ b/conan.lock
@@ -1,43 +1,43 @@
 {
     "version": "0.5",
     "requires": [
-        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
-        "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
-        "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1778091117.311",
-        "soci/4.0.3#fe32b9ad5eb47e79ab9e45a68f363945%1774450067.231",
-        "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
-        "secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
-        "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
-        "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1774398111.888",
-        "protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
-        "openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
-        "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
-        "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
-        "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
-        "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
-        "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848",
+        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
+        "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
+        "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
+        "soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
+        "snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
+        "secp256k1/0.7.1#b1f450b7f78a36fff75bb6934a356f3a%1782338841.3729",
+        "rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1782392413.075713",
+        "re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1782392402.431897",
+        "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
+        "openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
+        "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
+        "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%1768312129.152",
-        "grpc/1.81.0#2fb144aeb47e7f35c6ebb0e5f35bed31%1781620605.685",
-        "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1765850143.772",
-        "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772",
-        "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1774439234.681",
-        "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837",
-        "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778091165.282",
-        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
+        "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
+        "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#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
     ],
     "build_requires": [
-        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056",
-        "strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1774447376.964",
-        "protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
-        "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
+        "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
+        "strawberryperl/5.32.1.1#8d114504d172cfea8ea1662d09b6333e%1782395692.540639",
+        "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
+        "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162",
         "msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
-        "m4/1.4.19#4523e4347b55cd26ae918bd5770cab9a%1778062762.471",
-        "cmake/4.3.0#b939a42e98f593fb34d3a8c5cc860359%1774439249.183",
-        "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1774439233.447",
+        "m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259",
+        "cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091",
+        "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226",
         "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
         "autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
-        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196"
+        "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1782307147.395833"
     ],
     "python_requires": [],
     "overrides": {
@@ -57,7 +57,7 @@
             "boost/1.91.0"
         ],
         "lz4/[>=1.9.4 <2]": [
-            "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504"
+            "lz4/1.10.0#982d9b673900f665a1da109e09c17cab"
         ]
     },
     "config_requires": []
diff --git a/conan/lockfile/regenerate.sh b/conan/lockfile/regenerate.sh
index 1aa47628f0..98ee6f7c99 100755
--- a/conan/lockfile/regenerate.sh
+++ b/conan/lockfile/regenerate.sh
@@ -14,7 +14,7 @@ export CONAN_HOME="$TEMP_DIR"
 # Ensure that the xrplf remote is the first to be consulted, so any recipes we
 # patched are used. We also add it there to not created huge diff when the
 # official Conan Center Index is updated.
-conan remote add --force --index 0 xrplf https://conan.ripplex.io
+conan remote add --force --index 0 xrplf https://conan.xrplf.org/repository/conan/
 
 # Delete any existing lockfile.
 rm -f conan.lock
diff --git a/conanfile.py b/conanfile.py
index 5b78dc22e3..2733d4fc9c 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -28,10 +28,10 @@ class Xrpl(ConanFile):
 
     requires = [
         "ed25519/2015.03",
-        "grpc/1.81.0",
+        "grpc/1.81.1",
         "libarchive/3.8.7",
         "nudb/2.0.9",
-        "openssl/3.6.2",
+        "openssl/3.6.3",
         "secp256k1/0.7.1",
         "soci/4.0.3",
         "zlib/1.3.2",
diff --git a/docs/build/advanced_conan.md b/docs/build/advanced_conan.md
index aae17e385a..26b88ef186 100644
--- a/docs/build/advanced_conan.md
+++ b/docs/build/advanced_conan.md
@@ -34,7 +34,7 @@ higher index than the default Conan Center remote, so it is consulted first. You
 can do this by running:
 
 ```bash
-conan remote add --index 0 --force xrplf https://conan.ripplex.io
+conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/
 ```
 
 Alternatively, you can pull our recipes from the repository and export them locally:

From b9eee1d24537299acf3ea8dd33dd8ef9d9c6bd7a Mon Sep 17 00:00:00 2001
From: Mayukha Vadari 
Date: Fri, 26 Jun 2026 06:24:12 -0400
Subject: [PATCH 013/100] refactor: Rename (mostly keylet) functions to more
 closely match the docs (#7059)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 include/xrpl/ledger/helpers/EscrowHelpers.h   |   4 +-
 include/xrpl/protocol/Indexes.h               |  90 ++---
 .../xrpl/protocol/detail/ledger_entries.macro |  18 +-
 include/xrpl/protocol/nft.h                   |   4 +-
 include/xrpl/tx/paths/detail/StepChecks.h     |   6 +-
 src/libxrpl/ledger/Ledger.cpp                 |   4 +-
 src/libxrpl/ledger/View.cpp                   |   2 +-
 src/libxrpl/ledger/helpers/AMMHelpers.cpp     |   4 +-
 src/libxrpl/ledger/helpers/MPTokenHelpers.cpp |  30 +-
 src/libxrpl/ledger/helpers/NFTokenHelpers.cpp |  32 +-
 .../ledger/helpers/RippleStateHelpers.cpp     |  22 +-
 src/libxrpl/ledger/helpers/TokenHelpers.cpp   |  14 +-
 src/libxrpl/protocol/Indexes.cpp              |  46 +--
 src/libxrpl/tx/Transactor.cpp                 |   8 +-
 src/libxrpl/tx/invariants/InvariantCheck.cpp  |   4 +-
 .../tx/invariants/LoanBrokerInvariant.cpp     |   2 +-
 src/libxrpl/tx/invariants/MPTInvariant.cpp    |   2 +-
 src/libxrpl/tx/invariants/VaultInvariant.cpp  |   6 +-
 src/libxrpl/tx/paths/BookStep.cpp             |   2 +-
 src/libxrpl/tx/paths/DirectStep.cpp           |   4 +-
 src/libxrpl/tx/paths/MPTEndpointStep.cpp      |   2 +-
 src/libxrpl/tx/paths/XRPEndpointStep.cpp      |   2 +-
 .../tx/transactors/account/AccountDelete.cpp  |   4 +-
 .../tx/transactors/account/AccountSet.cpp     |   2 +-
 .../tx/transactors/account/SetRegularKey.cpp  |   2 +-
 .../tx/transactors/account/SignerListSet.cpp  |   6 +-
 .../tx/transactors/bridge/XChainBridge.cpp    |   2 +-
 .../tx/transactors/check/CheckCash.cpp        |   4 +-
 .../tx/transactors/check/CheckCreate.cpp      |   4 +-
 .../tx/transactors/dex/AMMClawback.cpp        |   2 +-
 src/libxrpl/tx/transactors/dex/AMMCreate.cpp  |   9 +-
 src/libxrpl/tx/transactors/dex/AMMDeposit.cpp |   5 +-
 .../tx/transactors/dex/AMMWithdraw.cpp        |   4 +-
 .../tx/transactors/dex/OfferCreate.cpp        |   8 +-
 .../tx/transactors/escrow/EscrowCancel.cpp    |   2 +-
 .../tx/transactors/escrow/EscrowCreate.cpp    |   4 +-
 .../tx/transactors/escrow/EscrowFinish.cpp    |   2 +-
 .../lending/LoanBrokerCoverClawback.cpp       |   6 +-
 .../lending/LoanBrokerCoverDeposit.cpp        |   4 +-
 .../lending/LoanBrokerCoverWithdraw.cpp       |   4 +-
 .../transactors/lending/LoanBrokerDelete.cpp  |   4 +-
 .../tx/transactors/lending/LoanBrokerSet.cpp  |   6 +-
 .../tx/transactors/lending/LoanDelete.cpp     |   4 +-
 .../tx/transactors/lending/LoanManage.cpp     |   4 +-
 .../tx/transactors/lending/LoanPay.cpp        |   6 +-
 .../tx/transactors/lending/LoanSet.cpp        |   6 +-
 .../tx/transactors/nft/NFTokenAcceptOffer.cpp |   6 +-
 .../tx/transactors/nft/NFTokenCancelOffer.cpp |   2 +-
 .../payment_channel/PaymentChannelCreate.cpp  |   2 +-
 src/libxrpl/tx/transactors/system/Change.cpp  |   2 +-
 .../tx/transactors/system/TicketCreate.cpp    |   2 +-
 src/libxrpl/tx/transactors/token/Clawback.cpp |   4 +-
 .../tx/transactors/token/MPTokenAuthorize.cpp |   9 +-
 .../token/MPTokenIssuanceCreate.cpp           |   2 +-
 .../token/MPTokenIssuanceDestroy.cpp          |   4 +-
 .../transactors/token/MPTokenIssuanceSet.cpp  |   4 +-
 src/libxrpl/tx/transactors/token/TrustSet.cpp |  14 +-
 .../tx/transactors/vault/VaultClawback.cpp    |   6 +-
 .../tx/transactors/vault/VaultCreate.cpp      |   2 +-
 .../tx/transactors/vault/VaultDelete.cpp      |   4 +-
 .../tx/transactors/vault/VaultDeposit.cpp     |   4 +-
 src/libxrpl/tx/transactors/vault/VaultSet.cpp |   4 +-
 .../tx/transactors/vault/VaultWithdraw.cpp    |   4 +-
 src/test/app/AccountDelete_test.cpp           |  16 +-
 src/test/app/Batch_test.cpp                   |   2 +-
 src/test/app/Check_test.cpp                   |  64 ++--
 src/test/app/Clawback_test.cpp                |   2 +-
 src/test/app/EscrowToken_test.cpp             |   7 +-
 src/test/app/FeeVote_test.cpp                 |   2 +-
 src/test/app/FixNFTokenPageLinks_test.cpp     |  72 ++--
 src/test/app/Flow_test.cpp                    |   2 +-
 src/test/app/Freeze_test.cpp                  |  12 +-
 src/test/app/Invariants_test.cpp              | 108 +++---
 src/test/app/LPTokenTransfer_test.cpp         |   4 +-
 src/test/app/LoanBroker_test.cpp              |  32 +-
 src/test/app/Loan_test.cpp                    | 118 +++---
 src/test/app/MPToken_test.cpp                 |   2 +-
 src/test/app/MultiSign_test.cpp               |   2 +-
 src/test/app/NFTokenAuth_test.cpp             |  26 +-
 src/test/app/NFTokenBurn_test.cpp             | 111 +++---
 src/test/app/NFTokenDir_test.cpp              |  16 +-
 src/test/app/NFToken_test.cpp                 | 342 +++++++++---------
 src/test/app/Offer_test.cpp                   |  10 +-
 src/test/app/Path_test.cpp                    |  12 +-
 src/test/app/PayChan_test.cpp                 |   2 +-
 src/test/app/PayStrand_test.cpp               |   4 +-
 src/test/app/PermissionedDEX_test.cpp         |   2 +-
 src/test/app/RCLValidations_test.cpp          |   2 +-
 src/test/app/SetAuth_test.cpp                 |   2 +-
 src/test/app/Vault_test.cpp                   |  58 +--
 src/test/jtx/impl/Env.cpp                     |   6 +-
 src/test/jtx/impl/TestHelpers.cpp             |   8 +-
 src/test/jtx/impl/balance.cpp                 |   2 +-
 src/test/jtx/impl/mpt.cpp                     |   8 +-
 src/test/rpc/AccountTx_test.cpp               |   2 +-
 src/test/rpc/LedgerEntry_test.cpp             |  10 +-
 src/test/rpc/Subscribe_test.cpp               |  17 +-
 src/tests/libxrpl/helpers/TxTest.cpp          |   6 +-
 src/tests/libxrpl/tx/AccountSet.cpp           |   6 +-
 src/xrpld/app/ledger/detail/BuildLedger.cpp   |   2 +-
 src/xrpld/app/ledger/detail/InboundLedger.cpp |   6 +-
 .../app/ledger/detail/LedgerPersistence.cpp   |   2 +-
 src/xrpld/app/ledger/detail/LocalTxs.cpp      |   2 +-
 src/xrpld/app/main/Application.cpp            |   7 +-
 src/xrpld/app/misc/detail/TxQ.cpp             |   2 +-
 src/xrpld/rpc/detail/AssetCache.cpp           |   2 +-
 src/xrpld/rpc/detail/Pathfinder.cpp           |   2 +-
 src/xrpld/rpc/detail/RPCHelpers.cpp           |   2 +-
 src/xrpld/rpc/handlers/VaultInfo.cpp          |   2 +-
 .../rpc/handlers/account/AccountInfo.cpp      |   2 +-
 .../rpc/handlers/account/AccountNFTs.cpp      |   6 +-
 .../rpc/handlers/account/AccountObjects.cpp   |   4 +-
 src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp |  10 +-
 .../rpc/handlers/orderbook/NFTOffersHelpers.h |   2 +-
 114 files changed, 825 insertions(+), 812 deletions(-)

diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h
index bdb83230eb..dc7c479c42 100644
--- a/include/xrpl/ledger/helpers/EscrowHelpers.h
+++ b/include/xrpl/ledger/helpers/EscrowHelpers.h
@@ -42,7 +42,7 @@ escrowUnlockApplyHelper(
     beast::Journal journal)
 {
     Issue const& issue = amount.get();
-    Keylet const trustLineKey = keylet::line(receiver, issue);
+    Keylet const trustLineKey = keylet::trustLine(receiver, issue);
     bool const recvLow = issuer > receiver;
     bool const senderIssuer = issuer == sender;
     bool const receiverIssuer = issuer == receiver;
@@ -175,7 +175,7 @@ escrowUnlockApplyHelper(
     bool const receiverIssuer = issuer == receiver;
 
     auto const mptID = amount.get().getMptID();
-    auto const issuanceKey = keylet::mptIssuance(mptID);
+    auto const issuanceKey = keylet::mptokenIssuance(mptID);
     if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && createAsset && !receiverIssuer)
     {
         if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)};
diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h
index 887a208ec6..75a2335f6f 100644
--- a/include/xrpl/protocol/Indexes.h
+++ b/include/xrpl/protocol/Indexes.h
@@ -68,21 +68,15 @@ skip(LedgerIndex ledger) noexcept;
 
 /** The (fixed) index of the object containing the ledger fees. */
 Keylet const&
-fees() noexcept;
+feeSettings() noexcept;
 
 /** The (fixed) index of the object containing the ledger negativeUNL. */
 Keylet const&
 negativeUNL() noexcept;
 
 /** The beginning of an order book */
-struct BookT
-{
-    explicit BookT() = default;
-
-    Keylet
-    operator()(Book const& b) const;
-};
-static BookT const kBook{};
+Keylet
+book(Book const& b);
 
 /** The index of a trust line for a given currency
 
@@ -93,12 +87,12 @@ static BookT const kBook{};
 */
 /** @{ */
 Keylet
-line(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept;
+trustLine(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept;
 
 inline Keylet
-line(AccountID const& id, Issue const& issue) noexcept
+trustLine(AccountID const& id, Issue const& issue) noexcept
 {
-    return line(id, issue.account, issue.currency);
+    return trustLine(id, issue.account, issue.currency);
 }
 /** @} */
 
@@ -119,37 +113,27 @@ Keylet
 quality(Keylet const& k, std::uint64_t q) noexcept;
 
 /** The directory for the next lower quality */
-struct NextT
-{
-    explicit NextT() = default;
-
-    Keylet
-    operator()(Keylet const& k) const;
-};
-static NextT const kNext{};
+Keylet
+next(Keylet const& k);
 
 /** A ticket belonging to an account */
-struct TicketT
+/** @{ */
+Keylet
+ticket(AccountID const& id, std::uint32_t ticketSeq);
+
+Keylet
+ticket(AccountID const& id, SeqProxy ticketSeq);
+
+inline Keylet
+ticket(uint256 const& key)
 {
-    explicit TicketT() = default;
-
-    Keylet
-    operator()(AccountID const& id, std::uint32_t ticketSeq) const;
-
-    Keylet
-    operator()(AccountID const& id, SeqProxy ticketSeq) const;
-
-    Keylet
-    operator()(uint256 const& key) const
-    {
-        return {ltTICKET, key};
-    }
-};
-static TicketT const kTicket{};
+    return {ltTICKET, key};
+}
+/** @} */
 
 /** A SignerList */
 Keylet
-signers(AccountID const& account) noexcept;
+signerList(AccountID const& account) noexcept;
 
 /** A Check */
 /** @{ */
@@ -209,7 +193,7 @@ escrow(AccountID const& src, std::uint32_t seq) noexcept;
 
 /** A PaymentChannel */
 Keylet
-payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept;
+payChannel(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept;
 
 /** NFT page keylets
 
@@ -221,22 +205,22 @@ payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept;
 /** @{ */
 /** A keylet for the owner's first possible NFT page. */
 Keylet
-nftpageMin(AccountID const& owner);
+nftokenPageMin(AccountID const& owner);
 
 /** A keylet for the owner's last possible NFT page. */
 Keylet
-nftpageMax(AccountID const& owner);
+nftokenPageMax(AccountID const& owner);
 
 Keylet
-nftpage(Keylet const& k, uint256 const& token);
+nftokenPage(Keylet const& k, uint256 const& token);
 /** @} */
 
 /** An offer from an account to buy or sell an NFT */
 Keylet
-nftoffer(AccountID const& owner, std::uint32_t seq);
+nftokenOffer(AccountID const& owner, std::uint32_t seq);
 
 inline Keylet
-nftoffer(uint256 const& offer)
+nftokenOffer(uint256 const& offer)
 {
     return {ltNFTOKEN_OFFER, offer};
 }
@@ -287,13 +271,13 @@ credential(uint256 const& key) noexcept
 }
 
 Keylet
-mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept;
+mptokenIssuance(std::uint32_t seq, AccountID const& issuer) noexcept;
 
 Keylet
-mptIssuance(MPTID const& issuanceID) noexcept;
+mptokenIssuance(MPTID const& issuanceID) noexcept;
 
 inline Keylet
-mptIssuance(uint256 const& issuanceKey)
+mptokenIssuance(uint256 const& issuanceKey)
 {
     return {ltMPTOKEN_ISSUANCE, issuanceKey};
 }
@@ -320,10 +304,10 @@ vault(uint256 const& vaultKey)
 }
 
 Keylet
-loanbroker(AccountID const& owner, std::uint32_t seq) noexcept;
+loanBroker(AccountID const& owner, std::uint32_t seq) noexcept;
 
 inline Keylet
-loanbroker(uint256 const& key)
+loanBroker(uint256 const& key)
 {
     return {ltLOAN_BROKER, key};
 }
@@ -376,11 +360,15 @@ struct KeyletDesc
 std::array, 6> const kDirectAccountKeylets{
     {{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
      {.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
-     {.function = &keylet::signers, .expectedLEName = jss::SignerList, .includeInTests = true},
+     {.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true},
      // It's normally impossible to create an item at nftpage_min, but
      // test it anyway, since the invariant checks for it.
-     {.function = &keylet::nftpageMin, .expectedLEName = jss::NFTokenPage, .includeInTests = true},
-     {.function = &keylet::nftpageMax, .expectedLEName = jss::NFTokenPage, .includeInTests = true},
+     {.function = &keylet::nftokenPageMin,
+      .expectedLEName = jss::NFTokenPage,
+      .includeInTests = true},
+     {.function = &keylet::nftokenPageMax,
+      .expectedLEName = jss::NFTokenPage,
+      .includeInTests = true},
      {.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
 
 MPTID
diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro
index 632038a9c5..e0ea1a61c3 100644
--- a/include/xrpl/protocol/detail/ledger_entries.macro
+++ b/include/xrpl/protocol/detail/ledger_entries.macro
@@ -21,7 +21,7 @@
 
 /** A ledger object which identifies an offer to buy or sell an NFT.
 
-    \sa keylet::nftoffer
+    \sa keylet::nftokenOffer
  */
 LEDGER_ENTRY(ltNFTOKEN_OFFER, 0x0037, NFTokenOffer, nft_offer, ({
     {sfOwner,                SoeRequired},
@@ -84,7 +84,7 @@ LEDGER_ENTRY(ltNEGATIVE_UNL, 0x004e, NegativeUNL, nunl, ({
 
 /** A ledger object which contains a list of NFTs
 
-    \sa keylet::nftpageMin, keylet::nftpageMax, keylet::nftpage
+    \sa keylet::nftokenPageMin, keylet::nftokenPageMax, keylet::nftokenPage
  */
 LEDGER_ENTRY(ltNFTOKEN_PAGE, 0x0050, NFTokenPage, nft_page, ({
     {sfPreviousPageMin,      SoeOptional},
@@ -96,7 +96,7 @@ LEDGER_ENTRY(ltNFTOKEN_PAGE, 0x0050, NFTokenPage, nft_page, ({
 
 /** A ledger object which contains a signer list for an account.
 
-    \sa keylet::signers
+    \sa keylet::signerList
  */
 // All fields are SoeRequired because there is always a SignerEntries.
 // If there are no SignerEntries the node is deleted.
@@ -112,7 +112,7 @@ LEDGER_ENTRY(ltSIGNER_LIST, 0x0053, SignerList, signer_list, ({
 
 /** A ledger object which describes a ticket.
 
-    \sa keylet::kTicket
+    \sa keylet::ticket
  */
 LEDGER_ENTRY(ltTICKET, 0x0054, Ticket, ticket, ({
     {sfAccount,              SoeRequired},
@@ -272,7 +272,7 @@ LEDGER_ENTRY(ltXCHAIN_OWNED_CLAIM_ID, 0x0071, XChainOwnedClaimID, xchain_owned_c
 
     @note Per Vinnie Falco this should be renamed to ltTRUST_LINE
 
-    \sa keylet::line
+    \sa keylet::trustLine
  */
 LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({
     {sfBalance,              SoeRequired},
@@ -292,7 +292,7 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({
 
     \note This is a singleton: only one such object exists in the ledger.
 
-    \sa keylet::fees
+    \sa keylet::feeSettings
  */
 LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
     // Old version uses raw numbers
@@ -346,7 +346,7 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({
 
 /** A ledger object describing a single unidirectional XRP payment channel.
 
-    \sa keylet::payChan
+    \sa keylet::payChannel
  */
 LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({
     {sfAccount,              SoeRequired},
@@ -384,7 +384,7 @@ LEDGER_ENTRY(ltAMM, 0x0079, AMM, amm, ({
 }))
 
 /** A ledger object which tracks MPTokenIssuance
-    \sa keylet::mptIssuance
+    \sa keylet::mptokenIssuance
  */
 LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
     {sfIssuer,                   SoeRequired},
@@ -499,7 +499,7 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
 
 /** A ledger object representing a loan broker
 
-    \sa keylet::loanbroker
+    \sa keylet::loanBroker
  */
 LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({
     {sfPreviousTxnID,        SoeRequired},
diff --git a/include/xrpl/protocol/nft.h b/include/xrpl/protocol/nft.h
index 1e79b3f285..54227dd1ea 100644
--- a/include/xrpl/protocol/nft.h
+++ b/include/xrpl/protocol/nft.h
@@ -52,7 +52,7 @@ getTransferFee(uint256 const& id)
 }
 
 inline std::uint32_t
-getSerial(uint256 const& id)
+getSequence(uint256 const& id)
 {
     std::uint32_t seq = 0;
     memcpy(&seq, id.begin() + 28, 4);
@@ -92,7 +92,7 @@ getTaxon(uint256 const& id)
 
     // The taxon cipher is just an XOR, so it is reversible by applying the
     // XOR a second time.
-    return cipheredTaxon(getSerial(id), toTaxon(taxon));
+    return cipheredTaxon(getSequence(id), toTaxon(taxon));
 }
 
 inline AccountID
diff --git a/include/xrpl/tx/paths/detail/StepChecks.h b/include/xrpl/tx/paths/detail/StepChecks.h
index fea9f90a31..4955c4f8e6 100644
--- a/include/xrpl/tx/paths/detail/StepChecks.h
+++ b/include/xrpl/tx/paths/detail/StepChecks.h
@@ -27,7 +27,7 @@ checkFreeze(
         }
     }
 
-    if (auto sle = view.read(keylet::line(src, dst, currency)))
+    if (auto sle = view.read(keylet::trustLine(src, dst, currency)))
     {
         if (sle->isFlag((dst > src) ? lsfHighFreeze : lsfLowFreeze))
         {
@@ -71,8 +71,8 @@ checkNoRipple(
     beast::Journal j)
 {
     // fetch the ripple lines into and out of this node
-    auto sleIn = view.read(keylet::line(prev, cur, currency));
-    auto sleOut = view.read(keylet::line(cur, next, currency));
+    auto sleIn = view.read(keylet::trustLine(prev, cur, currency));
+    auto sleOut = view.read(keylet::trustLine(cur, next, currency));
 
     if (!sleIn || !sleOut)
         return terNO_LINE;
diff --git a/src/libxrpl/ledger/Ledger.cpp b/src/libxrpl/ledger/Ledger.cpp
index 82956650e2..188fb3cd2c 100644
--- a/src/libxrpl/ledger/Ledger.cpp
+++ b/src/libxrpl/ledger/Ledger.cpp
@@ -180,7 +180,7 @@ Ledger::Ledger(
     }
 
     {
-        auto sle = std::make_shared(keylet::fees());
+        auto sle = std::make_shared(keylet::feeSettings());
         // Whether featureXRPFees is supported will depend on startup options.
         if (std::ranges::find(amendments, featureXRPFees) != amendments.end())
         {
@@ -560,7 +560,7 @@ Ledger::setup()
 
     try
     {
-        if (auto const sle = read(keylet::fees()))
+        if (auto const sle = read(keylet::feeSettings()))
         {
             bool oldFees = false;
             bool newFees = false;
diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp
index fdd7998609..ebac76a754 100644
--- a/src/libxrpl/ledger/View.cpp
+++ b/src/libxrpl/ledger/View.cpp
@@ -70,7 +70,7 @@ isVaultPseudoAccountFrozen(
         // LCOV_EXCL_STOP
     }
 
-    auto const mptIssuance = view.read(keylet::mptIssuance(mptShare.getMptID()));
+    auto const mptIssuance = view.read(keylet::mptokenIssuance(mptShare.getMptID()));
     if (mptIssuance == nullptr)
         return false;  // zero MPToken won't block deletion of MPTokenIssuance
 
diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp
index cacbfc9d58..7cd27a1f34 100644
--- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp
@@ -555,7 +555,7 @@ ammLPHolds(
     auto const currency = ammLPTCurrency(asset1, asset2);
     STAmount amount;
 
-    auto const sle = view.read(keylet::line(lpAccount, ammAccount, currency));
+    auto const sle = view.read(keylet::trustLine(lpAccount, ammAccount, currency));
     if (!sle)
     {
         amount.clear(Issue{currency, ammAccount});
@@ -647,7 +647,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
             }
             else if (
                 auto const sle =
-                    view.read(keylet::line(ammAccountID, issue.account, issue.currency));
+                    view.read(keylet::trustLine(ammAccountID, issue.account, issue.currency));
                 sle && !isFrozen(view, ammAccountID, issue.currency, issue.account))
             {
                 STAmount amount = (*sle)[sfBalance];
diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
index 8b3385471d..a1af63a80f 100644
--- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
@@ -40,7 +40,7 @@ namespace xrpl {
 bool
 isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue)
 {
-    if (auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID())))
+    if (auto const sle = view.read(keylet::mptokenIssuance(mptIssue.getMptID())))
         return sle->isFlag(lsfMPTLocked);
     return false;
 }
@@ -95,7 +95,7 @@ transferRate(ReadView const& view, MPTID const& issuanceID)
     // fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000
     // For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000
     // which represents 50% of 1,000,000,000
-    if (auto const sle = view.read(keylet::mptIssuance(issuanceID));
+    if (auto const sle = view.read(keylet::mptokenIssuance(issuanceID));
         sle && sle->isFieldPresent(sfTransferFee))
     {
         auto const fee = sle->getFieldU16(sfTransferFee);
@@ -110,7 +110,7 @@ transferRate(ReadView const& view, MPTID const& issuanceID)
 canAddHolding(ReadView const& view, MPTIssue const& mptIssue)
 {
     auto mptID = mptIssue.getMptID();
-    auto issuance = view.read(keylet::mptIssuance(mptID));
+    auto issuance = view.read(keylet::mptokenIssuance(mptID));
     if (!issuance)
     {
         return tecOBJECT_NOT_FOUND;
@@ -132,7 +132,7 @@ addEmptyHolding(
     beast::Journal journal)
 {
     auto const& mptID = mptIssue.getMptID();
-    auto const mpt = view.peek(keylet::mptIssuance(mptID));
+    auto const mpt = view.peek(keylet::mptokenIssuance(mptID));
     if (!mpt)
         return tefINTERNAL;  // LCOV_EXCL_LINE
     if (mpt->isFlag(lsfMPTLocked))
@@ -204,7 +204,7 @@ authorizeMPToken(
             return tecINSUFFICIENT_RESERVE;
 
         // Defensive check before we attempt to create MPToken for the issuer
-        auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID));
+        auto const mpt = view.read(keylet::mptokenIssuance(mptIssuanceID));
         if (!mpt || mpt->getAccountID(sfIssuer) == account)
         {
             // LCOV_EXCL_START
@@ -230,7 +230,7 @@ authorizeMPToken(
         return tesSUCCESS;
     }
 
-    auto const sleMptIssuance = view.read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleMptIssuance = view.read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleMptIssuance)
         return tecINTERNAL;  // LCOV_EXCL_LINE
 
@@ -308,7 +308,7 @@ requireAuth(
     AuthType authType,
     std::uint8_t depth)
 {
-    auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
+    auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
     auto const sleIssuance = view.read(mptID);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
@@ -406,7 +406,7 @@ enforceMPTokenAuthorization(
     XRPAmount const& priorBalance,  // for MPToken authorization
     beast::Journal j)
 {
-    auto const sleIssuance = view.read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = view.read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
         return tefINTERNAL;  // LCOV_EXCL_LINE
 
@@ -530,7 +530,7 @@ canTransfer(
     WaiveMPTCanTransfer waive,
     std::uint8_t depth)
 {
-    auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
+    auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
     auto const sleIssuance = view.read(mptID);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
@@ -584,7 +584,7 @@ canTrade(ReadView const& view, Asset const& asset, std::uint8_t depth)
     return asset.visit(
         [&](Issue const&) -> TER { return tesSUCCESS; },
         [&](MPTIssue const& mptIssue) -> TER {
-            auto const sleIssuance = view.read(keylet::mptIssuance(mptIssue.getMptID()));
+            auto const sleIssuance = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
             if (!sleIssuance)
                 return tecOBJECT_NOT_FOUND;
             if (!sleIssuance->isFlag(lsfMPTCanTrade))
@@ -638,7 +638,7 @@ TER
 lockEscrowMPT(ApplyView& view, AccountID const& sender, STAmount const& amount, beast::Journal j)
 {
     auto const mptIssue = amount.get();
-    auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
+    auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
     auto sleIssuance = view.peek(mptID);
     if (!sleIssuance)
     {  // LCOV_EXCL_START
@@ -743,7 +743,7 @@ unlockEscrowMPT(
 
     auto const& issuer = netAmount.getIssuer();
     auto const& mptIssue = netAmount.get();
-    auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
+    auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
     auto sleIssuance = view.peek(mptID);
     if (!sleIssuance)
     {  // LCOV_EXCL_START
@@ -927,7 +927,7 @@ checkCreateMPT(
     if (mptIssue.getIssuer() == holder)
         return tesSUCCESS;
 
-    auto const mptIssuanceID = keylet::mptIssuance(mptIssue.getMptID());
+    auto const mptIssuanceID = keylet::mptokenIssuance(mptIssue.getMptID());
     auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder);
     if (!view.exists(mptokenID))
     {
@@ -963,7 +963,7 @@ availableMPTAmount(SLE const& sleIssuance)
 std::int64_t
 availableMPTAmount(ReadView const& view, MPTID const& mptID)
 {
-    auto const sle = view.read(keylet::mptIssuance(mptID));
+    auto const sle = view.read(keylet::mptokenIssuance(mptID));
     if (!sle)
         Throw(transHuman(tecINTERNAL));
     return availableMPTAmount(*sle);
@@ -987,7 +987,7 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue)
 {
     STAmount amount{issue};
 
-    auto const sle = view.read(keylet::mptIssuance(issue));
+    auto const sle = view.read(keylet::mptokenIssuance(issue));
     if (!sle)
         return amount;
     auto const available = availableMPTAmount(*sle);
diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
index 5b467db796..af429358bb 100644
--- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
@@ -44,8 +44,8 @@ namespace xrpl::nft {
 static SLE::const_pointer
 locatePage(ReadView const& view, AccountID const& owner, uint256 const& id)
 {
-    auto const first = keylet::nftpage(keylet::nftpageMin(owner), id);
-    auto const last = keylet::nftpageMax(owner);
+    auto const first = keylet::nftokenPage(keylet::nftokenPageMin(owner), id);
+    auto const last = keylet::nftokenPageMax(owner);
 
     // This NFT can only be found in the first page with a key that's strictly
     // greater than `first`, so look for that, up until the maximum possible
@@ -57,8 +57,8 @@ locatePage(ReadView const& view, AccountID const& owner, uint256 const& id)
 static SLE::pointer
 locatePage(ApplyView& view, AccountID const& owner, uint256 const& id)
 {
-    auto const first = keylet::nftpage(keylet::nftpageMin(owner), id);
-    auto const last = keylet::nftpageMax(owner);
+    auto const first = keylet::nftokenPage(keylet::nftokenPageMin(owner), id);
+    auto const last = keylet::nftokenPageMax(owner);
 
     // This NFT can only be found in the first page with a key that's strictly
     // greater than `first`, so look for that, up until the maximum possible
@@ -74,9 +74,9 @@ getPageForToken(
     uint256 const& id,
     std::function const& createCallback)
 {
-    auto const base = keylet::nftpageMin(owner);
-    auto const first = keylet::nftpage(base, id);
-    auto const last = keylet::nftpageMax(owner);
+    auto const base = keylet::nftokenPageMin(owner);
+    auto const first = keylet::nftokenPage(base, id);
+    auto const last = keylet::nftokenPageMax(owner);
 
     // This NFT can only be found in the first page with a key that's strictly
     // greater than `first`, so look for that, up until the maximum possible
@@ -182,7 +182,7 @@ getPageForToken(
         ? narr[kDirMaxTokensPerPage - 1].getFieldH256(sfNFTokenID).next()
         : carr[0].getFieldH256(sfNFTokenID);
 
-    auto np = std::make_shared(keylet::nftpage(base, tokenIDForNewPage));
+    auto np = std::make_shared(keylet::nftokenPage(base, tokenIDForNewPage));
     XRPL_ASSERT(np->key() > base.key, "xrpl::nft::getPageForToken : valid NFT page index");
     np->setFieldArray(sfNFTokens, narr);
     np->setFieldH256(sfNextPageMin, cp->key());
@@ -597,7 +597,7 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t
         // deleting during iteration.
         for (int i = offerIndexes.size() - 1; i >= 0; --i)
         {
-            if (auto const offer = view.peek(keylet::nftoffer(offerIndexes[i])))
+            if (auto const offer = view.peek(keylet::nftokenOffer(offerIndexes[i])))
             {
                 if (deleteTokenOffer(view, offer))
                 {
@@ -651,11 +651,11 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner)
 {
     bool didRepair = false;
 
-    auto const last = keylet::nftpageMax(owner);
+    auto const last = keylet::nftokenPageMax(owner);
 
     SLE::pointer page = view.peek(Keylet(
         ltNFTOKEN_PAGE,
-        view.succ(keylet::nftpageMin(owner).key, last.key.next()).value_or(last.key)));
+        view.succ(keylet::nftokenPageMin(owner).key, last.key.next()).value_or(last.key)));
 
     if (!page)
         return didRepair;
@@ -839,10 +839,10 @@ tokenOfferCreatePreclaim(
         if (view.rules().enabled(featureNFTokenMintOffer))
         {
             if (nftIssuer != amount.getIssuer() &&
-                !view.read(keylet::line(nftIssuer, amount.get())))
+                !view.read(keylet::trustLine(nftIssuer, amount.get())))
                 return tecNO_LINE;
         }
-        else if (!view.exists(keylet::line(nftIssuer, amount.get())))
+        else if (!view.exists(keylet::trustLine(nftIssuer, amount.get())))
         {
             return tecNO_LINE;
         }
@@ -934,7 +934,7 @@ tokenOfferCreateApply(
         priorBalance < view.fees().accountReserve((*acct)[sfOwnerCount] + 1))
         return tecINSUFFICIENT_RESERVE;
 
-    auto const offerID = keylet::nftoffer(acctID, seqProxy.value());
+    auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value());
 
     // Create the offer:
     {
@@ -1020,7 +1020,7 @@ checkTrustlineAuthorized(
 
         if (issuerAccount->isFlag(lsfRequireAuth))
         {
-            auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
+            auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
 
             if (!trustLine)
             {
@@ -1070,7 +1070,7 @@ checkTrustlineDeepFrozen(
             return tesSUCCESS;
         }
 
-        auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
+        auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
 
         if (!trustLine)
         {
diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp
index 7ed7ab8fc4..5a2995c030 100644
--- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp
@@ -47,7 +47,7 @@ creditLimit(
 {
     STAmount result(Issue{currency, account});
 
-    auto sleRippleState = view.read(keylet::line(account, issuer, currency));
+    auto sleRippleState = view.read(keylet::trustLine(account, issuer, currency));
 
     if (sleRippleState)
     {
@@ -78,7 +78,7 @@ creditBalance(
 {
     STAmount result(Issue{currency, account});
 
-    auto sleRippleState = view.read(keylet::line(account, issuer, currency));
+    auto sleRippleState = view.read(keylet::trustLine(account, issuer, currency));
 
     if (sleRippleState)
     {
@@ -114,7 +114,7 @@ isIndividualFrozen(
     if (issuer != account)
     {
         // Check if the issuer froze the line
-        auto const sle = view.read(keylet::line(account, issuer, currency));
+        auto const sle = view.read(keylet::trustLine(account, issuer, currency));
         if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
             return true;
     }
@@ -138,7 +138,7 @@ isFrozen(
     if (issuer != account)
     {
         // Check if the issuer froze the line
-        sle = view.read(keylet::line(account, issuer, currency));
+        sle = view.read(keylet::trustLine(account, issuer, currency));
         if (sle && sle->isFlag((issuer > account) ? lsfHighFreeze : lsfLowFreeze))
             return true;
     }
@@ -162,7 +162,7 @@ isDeepFrozen(
         return false;
     }
 
-    auto const sle = view.read(keylet::line(account, issuer, currency));
+    auto const sle = view.read(keylet::trustLine(account, issuer, currency));
     if (!sle)
     {
         return false;
@@ -403,7 +403,7 @@ issueIOU(
 
     bool const bSenderHigh = issue.account > account;
 
-    auto const index = keylet::line(issue.account, account, issue.currency);
+    auto const index = keylet::trustLine(issue.account, account, issue.currency);
 
     if (auto state = view.peek(index))
     {
@@ -497,7 +497,7 @@ redeemIOU(
 
     bool const bSenderHigh = account > issue.account;
 
-    if (auto state = view.peek(keylet::line(account, issue.account, issue.currency)))
+    if (auto state = view.peek(keylet::trustLine(account, issue.account, issue.currency)))
     {
         STAmount finalBalance = state->getFieldAmount(sfBalance);
 
@@ -558,7 +558,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
     if (isXRP(issue) || issue.account == account)
         return tesSUCCESS;
 
-    auto const trustLine = view.read(keylet::line(account, issue.account, issue.currency));
+    auto const trustLine = view.read(keylet::trustLine(account, issue.account, issue.currency));
     // If account has no line, and this is a strong check, fail
     if (!trustLine && authType == AuthType::StrongAuth)
         return tecNO_LINE;
@@ -596,7 +596,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
     auto const isRippleDisabled = [&](AccountID account) -> bool {
         // Line might not exist, but some transfers can create it. If this
         // is the case, just check the default ripple on the issuer account.
-        auto const line = view.read(keylet::line(account, issue));
+        auto const line = view.read(keylet::trustLine(account, issue));
         if (line)
         {
             bool const issuerHigh = issuerId > account;
@@ -638,7 +638,7 @@ addEmptyHolding(
     auto const& srcId = issuerId;
     auto const& dstId = accountID;
     auto const high = srcId > dstId;
-    auto const index = keylet::line(srcId, dstId, currency);
+    auto const index = keylet::trustLine(srcId, dstId, currency);
     auto const sleSrc = view.peek(keylet::account(srcId));
     auto const sleDst = view.peek(keylet::account(dstId));
     if (!sleDst || !sleSrc)
@@ -696,7 +696,7 @@ removeEmptyHolding(
     // If the account is the issuer, then no line should exist. Check anyway.
     // If a line does exist, it will get deleted. If not, return success.
     bool const accountIsIssuer = accountID == issue.account;
-    auto const line = view.peek(keylet::line(accountID, issue));
+    auto const line = view.peek(keylet::trustLine(accountID, issue));
     if (!line)
         return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND;
     if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::kZero)
diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
index 7191456868..f0a0220e1e 100644
--- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
@@ -179,7 +179,7 @@ getLineIfUsable(
     FreezeHandling zeroIfFrozen,
     beast::Journal j)
 {
-    auto sle = view.read(keylet::line(account, issuer, currency));
+    auto sle = view.read(keylet::trustLine(account, issuer, currency));
 
     if (!sle)
     {
@@ -320,7 +320,7 @@ accountHolds(
     {
         // if the account is the issuer, and the issuance exists, their limit is
         // the issuance limit minus the outstanding value
-        auto const issuance = view.read(keylet::mptIssuance(mptIssue.getMptID()));
+        auto const issuance = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
 
         if (!issuance)
         {
@@ -357,7 +357,7 @@ accountHolds(
         }
         else if (zeroIfUnauthorized == AuthHandling::ZeroIfUnauthorized)
         {
-            auto const sleIssuance = view.read(keylet::mptIssuance(mptIssue.getMptID()));
+            auto const sleIssuance = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
 
             // if auth is enabled on the issuance and mpt is not authorized,
             // clear amount
@@ -568,7 +568,7 @@ directSendNoFeeIOU(
     XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::directSendNoFeeIOU : sender is not receiver");
 
     bool const bSenderHigh = uSenderID > uReceiverID;
-    auto const index = keylet::line(uSenderID, uReceiverID, currency);
+    auto const index = keylet::trustLine(uSenderID, uReceiverID, currency);
 
     XRPL_ASSERT(
         !isXRP(uSenderID) && uSenderID != noAccount(),
@@ -1066,7 +1066,7 @@ directSendNoFeeMPT(
     beast::Journal j)
 {
     // Do not check MPT authorization here - it must have been checked earlier
-    auto const mptID = keylet::mptIssuance(saAmount.get().getMptID());
+    auto const mptID = keylet::mptokenIssuance(saAmount.get().getMptID());
     auto const& issuer = saAmount.getIssuer();
     auto sleIssuance = view.peek(mptID);
     if (!sleIssuance)
@@ -1158,7 +1158,7 @@ directSendNoLimitMPT(
     // Safe to get MPT since directSendNoLimitMPT is only called by accountSendMPT
     auto const& issuer = saAmount.getIssuer();
 
-    auto const sle = view.read(keylet::mptIssuance(saAmount.get().getMptID()));
+    auto const sle = view.read(keylet::mptokenIssuance(saAmount.get().getMptID()));
     if (!sle)
         return tecOBJECT_NOT_FOUND;
 
@@ -1215,7 +1215,7 @@ directSendNoLimitMultiMPT(
 {
     auto const& issuer = mptIssue.getIssuer();
 
-    auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()));
+    auto const sle = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
     if (!sle)
         return tecOBJECT_NOT_FOUND;
 
diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp
index ae29bd3297..a49f7b85ee 100644
--- a/src/libxrpl/protocol/Indexes.cpp
+++ b/src/libxrpl/protocol/Indexes.cpp
@@ -218,7 +218,7 @@ amendments() noexcept
 }
 
 Keylet const&
-fees() noexcept
+feeSettings() noexcept
 {
     static Keylet const kRet{ltFEE_SETTINGS, indexHash(LedgerNameSpace::FeeSettings)};
     return kRet;
@@ -232,18 +232,18 @@ negativeUNL() noexcept
 }
 
 Keylet
-BookT::operator()(Book const& b) const
+book(Book const& b)
 {
     return {ltDIR_NODE, getBookBase(b)};
 }
 
 Keylet
-line(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept
+trustLine(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept
 {
     // There is code in TrustSet that calls us with id0 == id1, to allow users
     // to locate and delete such "weird" trustlines. If we remove that code, we
     // could enable this assert:
-    // XRPL_ASSERT(id0 != id1, "xrpl::keylet::line : accounts must be
+    // XRPL_ASSERT(id0 != id1, "xrpl::keylet::trustLine : accounts must be
     // different");
 
     // A trust line is shared between two accounts; while we typically think
@@ -285,20 +285,20 @@ quality(Keylet const& k, std::uint64_t q) noexcept
 }
 
 Keylet
-NextT::operator()(Keylet const& k) const
+next(Keylet const& k)
 {
-    XRPL_ASSERT(k.type == ltDIR_NODE, "xrpl::keylet::NextT::operator() : valid input type");
+    XRPL_ASSERT(k.type == ltDIR_NODE, "xrpl::keylet::next : valid input type");
     return {ltDIR_NODE, getQualityNext(k.key)};
 }
 
 Keylet
-TicketT::operator()(AccountID const& id, std::uint32_t ticketSeq) const
+ticket(AccountID const& id, std::uint32_t ticketSeq)
 {
     return {ltTICKET, getTicketIndex(id, ticketSeq)};
 }
 
 Keylet
-TicketT::operator()(AccountID const& id, SeqProxy ticketSeq) const
+ticket(AccountID const& id, SeqProxy ticketSeq)
 {
     return {ltTICKET, getTicketIndex(id, ticketSeq)};
 }
@@ -307,15 +307,15 @@ TicketT::operator()(AccountID const& id, SeqProxy ticketSeq) const
 // else. If we ever support multiple pages of signer lists, this would be the
 // keylet used to locate them.
 static Keylet
-signers(AccountID const& account, std::uint32_t page) noexcept
+signerList(AccountID const& account, std::uint32_t page) noexcept
 {
     return {ltSIGNER_LIST, indexHash(LedgerNameSpace::SignerList, account, page)};
 }
 
 Keylet
-signers(AccountID const& account) noexcept
+signerList(AccountID const& account) noexcept
 {
-    return signers(account, 0);
+    return signerList(account, 0);
 }
 
 Keylet
@@ -375,13 +375,13 @@ escrow(AccountID const& src, std::uint32_t seq) noexcept
 }
 
 Keylet
-payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept
+payChannel(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept
 {
     return {ltPAYCHAN, indexHash(LedgerNameSpace::XRPPaymentChannel, src, dst, seq)};
 }
 
 Keylet
-nftpageMin(AccountID const& owner)
+nftokenPageMin(AccountID const& owner)
 {
     std::array buf{};
     std::memcpy(buf.data(), owner.data(), owner.size());
@@ -389,7 +389,7 @@ nftpageMin(AccountID const& owner)
 }
 
 Keylet
-nftpageMax(AccountID const& owner)
+nftokenPageMax(AccountID const& owner)
 {
     uint256 id = nft::kPageMask;
     std::memcpy(id.data(), owner.data(), owner.size());
@@ -397,14 +397,14 @@ nftpageMax(AccountID const& owner)
 }
 
 Keylet
-nftpage(Keylet const& k, uint256 const& token)
+nftokenPage(Keylet const& k, uint256 const& token)
 {
-    XRPL_ASSERT(k.type == ltNFTOKEN_PAGE, "xrpl::keylet::nftpage : valid input type");
+    XRPL_ASSERT(k.type == ltNFTOKEN_PAGE, "xrpl::keylet::nftokenPage : valid input type");
     return {ltNFTOKEN_PAGE, (k.key & ~nft::kPageMask) + (token & nft::kPageMask)};
 }
 
 Keylet
-nftoffer(AccountID const& owner, std::uint32_t seq)
+nftokenOffer(AccountID const& owner, std::uint32_t seq)
 {
     return {ltNFTOKEN_OFFER, indexHash(LedgerNameSpace::NftokenOffer, owner, seq)};
 }
@@ -518,13 +518,13 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept
 }
 
 Keylet
-mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept
+mptokenIssuance(std::uint32_t seq, AccountID const& issuer) noexcept
 {
-    return mptIssuance(makeMptID(seq, issuer));
+    return mptokenIssuance(makeMptID(seq, issuer));
 }
 
 Keylet
-mptIssuance(MPTID const& issuanceID) noexcept
+mptokenIssuance(MPTID const& issuanceID) noexcept
 {
     return {ltMPTOKEN_ISSUANCE, indexHash(LedgerNameSpace::MPTokenIssuance, issuanceID)};
 }
@@ -532,7 +532,7 @@ mptIssuance(MPTID const& issuanceID) noexcept
 Keylet
 mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept
 {
-    return mptoken(mptIssuance(issuanceID).key, holder);
+    return mptoken(mptokenIssuance(issuanceID).key, holder);
 }
 
 Keylet
@@ -554,9 +554,9 @@ vault(AccountID const& owner, std::uint32_t seq) noexcept
 }
 
 Keylet
-loanbroker(AccountID const& owner, std::uint32_t seq) noexcept
+loanBroker(AccountID const& owner, std::uint32_t seq) noexcept
 {
-    return loanbroker(indexHash(LedgerNameSpace::LoanBroker, owner, seq));
+    return loanBroker(indexHash(LedgerNameSpace::LoanBroker, owner, seq));
 }
 
 Keylet
diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp
index 2ff24d92b5..3a10b7124f 100644
--- a/src/libxrpl/tx/Transactor.cpp
+++ b/src/libxrpl/tx/Transactor.cpp
@@ -528,7 +528,7 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
         }
 
         // Transaction can never succeed if the Ticket is not in the ledger.
-        if (!view.exists(keylet::kTicket(id, tSeqProx)))
+        if (!view.exists(keylet::ticket(id, tSeqProx)))
         {
             JLOG(j.trace()) << "applyTransaction: ticket already used or never created "
                             << "a_seq=" << aSeq << " t_seq=" << tSeqProx;
@@ -593,7 +593,7 @@ Transactor::ticketDelete(
 {
     // Delete the Ticket, adjust the account root ticket count, and
     // reduce the owner count.
-    SLE::pointer const sleTicket = view.peek(keylet::kTicket(ticketIndex));
+    SLE::pointer const sleTicket = view.peek(keylet::ticket(ticketIndex));
     if (!sleTicket)
     {
         // LCOV_EXCL_START
@@ -851,7 +851,7 @@ Transactor::checkMultiSign(
     beast::Journal const j)
 {
     // Get id's SignerList and Quorum.
-    STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signers(id));
+    STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signerList(id));
     // If the signer list doesn't exist the account is not multi-signing.
     if (!sleAccountSigners)
     {
@@ -1031,7 +1031,7 @@ removeExpiredNFTokenOffers(
 
     for (auto const& index : offers)
     {
-        if (auto const offer = view.peek(keylet::nftoffer(index)))
+        if (auto const offer = view.peek(keylet::nftokenOffer(index)))
         {
             nft::deleteTokenOffer(view, offer);
             if (++removed == kExpiredOfferRemoveLimit)
diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp
index e29a9fe661..231705efaf 100644
--- a/src/libxrpl/tx/invariants/InvariantCheck.cpp
+++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp
@@ -518,8 +518,8 @@ AccountRootsDeletedClean::finalize(
             // checked above as entries in directAccountKeylets. This uses
             // view.succ() to check for any NFT pages in between the two
             // endpoints.
-            Keylet const first = keylet::nftpageMin(accountID);
-            Keylet const last = keylet::nftpageMax(accountID);
+            Keylet const first = keylet::nftokenPageMin(accountID);
+            Keylet const last = keylet::nftokenPageMax(accountID);
 
             std::optional key = view.succ(first.key, last.key.next());
 
diff --git a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
index 8586a27be3..d239acd417 100644
--- a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
+++ b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
@@ -130,7 +130,7 @@ ValidLoanBroker::finalize(
     for (auto const& [brokerID, broker] : brokers_)
     {
         auto const& after =
-            broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID));
+            broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanBroker(brokerID));
 
         if (!after)
         {
diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp
index a2369cc1b9..1176594bbf 100644
--- a/src/libxrpl/tx/invariants/MPTInvariant.cpp
+++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp
@@ -551,7 +551,7 @@ ValidMPTTransfer::finalize(
         std::uint16_t senders = 0;
         std::uint16_t receivers = 0;
         bool invalidTransfer = false;
-        auto const sleIssuance = view.read(keylet::mptIssuance(mptID));
+        auto const sleIssuance = view.read(keylet::mptokenIssuance(mptID));
         if (!sleIssuance)
         {
             continue;
diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp
index 80b8f36bd9..84814977db 100644
--- a/src/libxrpl/tx/invariants/VaultInvariant.cpp
+++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp
@@ -199,7 +199,7 @@ ValidVault::deltaAssets(AccountID const& id) const
             {
                 if (isXRP(issue))
                     return lookup(keylet::account(id).key);
-                auto result = lookup(keylet::line(id, issue).key);
+                auto result = lookup(keylet::trustLine(id, issue).key);
                 // Trust-line balance is stored from the low-account's perspective;
                 // negate if id is the high account so the delta is in id's terms.
                 if (result && id > issue.getIssuer())
@@ -238,7 +238,7 @@ ValidVault::deltaShares(AccountID const& id) const
     auto const& afterVault = afterVault_[0];
     auto const it = [&]() {
         if (id == afterVault.pseudoId)
-            return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key);
+            return deltas_.find(keylet::mptokenIssuance(afterVault.shareMPTID).key);
         return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
     }();
 
@@ -411,7 +411,7 @@ ValidVault::finalize(
                 return e;
         }
 
-        auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID));
+        auto const sleShares = view.read(keylet::mptokenIssuance(afterVault.shareMPTID));
 
         return sleShares ? std::optional(Shares::make(*sleShares)) : std::nullopt;
     }();
diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp
index 448172872a..adb73f126e 100644
--- a/src/libxrpl/tx/paths/BookStep.cpp
+++ b/src/libxrpl/tx/paths/BookStep.cpp
@@ -1359,7 +1359,7 @@ BookStep::check(StrandContext const& ctx) const
 
             auto const err = book_.in.visit(
                 [&](Issue const& issue) -> std::optional {
-                    auto sle = view.read(keylet::line(*prev, cur, issue.currency));
+                    auto sle = view.read(keylet::trustLine(*prev, cur, issue.currency));
                     if (!sle)
                         return terNO_LINE;
                     if (sle->isFlag((cur > *prev) ? lsfHighNoRipple : lsfLowNoRipple))
diff --git a/src/libxrpl/tx/paths/DirectStep.cpp b/src/libxrpl/tx/paths/DirectStep.cpp
index cc6e75ea3d..f8f12bd421 100644
--- a/src/libxrpl/tx/paths/DirectStep.cpp
+++ b/src/libxrpl/tx/paths/DirectStep.cpp
@@ -344,7 +344,7 @@ DirectIPaymentStep::quality(ReadView const& sb, QualityDirection qDir) const
     if (src_ == dst_)
         return QUALITY_ONE;
 
-    auto const sle = sb.read(keylet::line(dst_, src_, currency_));
+    auto const sle = sb.read(keylet::trustLine(dst_, src_, currency_));
 
     if (!sle)
         return QUALITY_ONE;
@@ -420,7 +420,7 @@ DirectIPaymentStep::check(StrandContext const& ctx, SLE::const_ref sleSrc) const
     // Since this is a payment a trust line must be present.  Perform all
     // trust line related checks.
     {
-        auto const sleLine = ctx.view.read(keylet::line(src_, dst_, currency_));
+        auto const sleLine = ctx.view.read(keylet::trustLine(src_, dst_, currency_));
         if (!sleLine)
         {
             JLOG(j_.trace()) << "DirectStepI: No credit line. " << *this;
diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp
index 7dcd6d9241..a47cfa15a5 100644
--- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp
+++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp
@@ -434,7 +434,7 @@ MPTEndpointStep::maxPaymentFlow(ReadView const& sb) const
         return {toAmount(maxFlow), DebtDirection::Redeems};
 
     // From an issuer to a holder
-    if (auto const sle = sb.read(keylet::mptIssuance(mptIssue_)))
+    if (auto const sle = sb.read(keylet::mptokenIssuance(mptIssue_)))
     {
         // If issuer is the source account, and it is direct payment then
         // MPTEndpointStep is the only step. Provide available maxFlow.
diff --git a/src/libxrpl/tx/paths/XRPEndpointStep.cpp b/src/libxrpl/tx/paths/XRPEndpointStep.cpp
index efdad92791..05d893a50d 100644
--- a/src/libxrpl/tx/paths/XRPEndpointStep.cpp
+++ b/src/libxrpl/tx/paths/XRPEndpointStep.cpp
@@ -203,7 +203,7 @@ private:
         {
             return ctx.strandDeliver.visit(
                 [&](Issue const& issue) {
-                    if (!ctx.view.exists(keylet::line(acc, issue)))
+                    if (!ctx.view.exists(keylet::trustLine(acc, issue)))
                         return -1;
                     return 0;
                 },
diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp
index 833f1c8b25..254733a618 100644
--- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp
+++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp
@@ -260,8 +260,8 @@ AccountDelete::preclaim(PreclaimContext const& ctx)
         return tecHAS_OBLIGATIONS;
 
     // If the account owns any NFTs it cannot be deleted.
-    Keylet const first = keylet::nftpageMin(account);
-    Keylet const last = keylet::nftpageMax(account);
+    Keylet const first = keylet::nftokenPageMin(account);
+    Keylet const last = keylet::nftokenPageMax(account);
 
     auto const cp = ctx.view.read(
         Keylet(ltNFTOKEN_PAGE, ctx.view.succ(first.key, last.key.next()).value_or(last.key)));
diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp b/src/libxrpl/tx/transactors/account/AccountSet.cpp
index 36a7e7419f..dfe7ec5b5f 100644
--- a/src/libxrpl/tx/transactors/account/AccountSet.cpp
+++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp
@@ -315,7 +315,7 @@ AccountSet::doApply()
             return tecNEED_MASTER_KEY;
         }
 
-        if ((!sle->isFieldPresent(sfRegularKey)) && (!view().peek(keylet::signers(accountID_))))
+        if ((!sle->isFieldPresent(sfRegularKey)) && (!view().peek(keylet::signerList(accountID_))))
         {
             // Account has no regular key or multi-signer signer list.
             return tecNO_ALTERNATIVE_KEY;
diff --git a/src/libxrpl/tx/transactors/account/SetRegularKey.cpp b/src/libxrpl/tx/transactors/account/SetRegularKey.cpp
index 08ccf5e7ab..f408f31690 100644
--- a/src/libxrpl/tx/transactors/account/SetRegularKey.cpp
+++ b/src/libxrpl/tx/transactors/account/SetRegularKey.cpp
@@ -67,7 +67,7 @@ SetRegularKey::doApply()
     else
     {
         // Account has disabled master key and no multi-signer signer list.
-        if (sle->isFlag(lsfDisableMaster) && !view().peek(keylet::signers(accountID_)))
+        if (sle->isFlag(lsfDisableMaster) && !view().peek(keylet::signerList(accountID_)))
             return tecNO_ALTERNATIVE_KEY;
 
         sle->makeFieldAbsent(sfRegularKey);
diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp b/src/libxrpl/tx/transactors/account/SignerListSet.cpp
index cedb9ace78..2f95d32664 100644
--- a/src/libxrpl/tx/transactors/account/SignerListSet.cpp
+++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp
@@ -232,7 +232,7 @@ SignerListSet::removeFromLedger(
 {
     auto const accountKeylet = keylet::account(account);
     auto const ownerDirKeylet = keylet::ownerDir(account);
-    auto const signerListKeylet = keylet::signers(account);
+    auto const signerListKeylet = keylet::signerList(account);
 
     return removeSignersFromLedger(
         registry, view, accountKeylet, ownerDirKeylet, signerListKeylet, j);
@@ -302,7 +302,7 @@ SignerListSet::replaceSignerList()
 {
     auto const accountKeylet = keylet::account(accountID_);
     auto const ownerDirKeylet = keylet::ownerDir(accountID_);
-    auto const signerListKeylet = keylet::signers(accountID_);
+    auto const signerListKeylet = keylet::signerList(accountID_);
 
     // This may be either a create or a replace.  Preemptively remove any
     // old signer list.  May reduce the reserve, so this is done before
@@ -367,7 +367,7 @@ SignerListSet::destroySignerList()
         return tecNO_ALTERNATIVE_KEY;
 
     auto const ownerDirKeylet = keylet::ownerDir(accountID_);
-    auto const signerListKeylet = keylet::signers(accountID_);
+    auto const signerListKeylet = keylet::signerList(accountID_);
     return removeSignersFromLedger(
         ctx_.registry, view(), accountKeylet, ownerDirKeylet, signerListKeylet, j_);
 }
diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp
index 273beaea0f..1e9e0bfc61 100644
--- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp
+++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp
@@ -759,7 +759,7 @@ getSignersListAndQuorum(ReadView const& view, SLE const& sleBridge, beast::Journ
         return {r, q, tecINTERNAL};
     }
 
-    auto const sleS = view.read(keylet::signers(sleBridge[sfAccount]));
+    auto const sleS = view.read(keylet::signerList(sleBridge[sfAccount]));
     if (!sleS)
     {
         return {r, q, tecXCHAIN_NO_SIGNERS_LIST};
diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp
index c5813ae42d..80ef742f52 100644
--- a/src/libxrpl/tx/transactors/check/CheckCash.cpp
+++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp
@@ -192,7 +192,7 @@ CheckCash::preclaim(PreclaimContext const& ctx)
                 [&](Issue const& issue) -> TER {
                     Currency const currency{issue.currency};
                     auto const sleTrustLine =
-                        ctx.view.read(keylet::line(dstId, issuerId, currency));
+                        ctx.view.read(keylet::trustLine(dstId, issuerId, currency));
 
                     auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
                     if (!sleIssuer)
@@ -411,7 +411,7 @@ CheckCash::doApply()
                     // If a trust line does not exist yet create one.
                     Issue const& trustLineIssue = issue;
                     AccountID const truster = deliverIssuer == accountID_ ? srcId : accountID_;
-                    trustLineKey = keylet::line(truster, trustLineIssue);
+                    trustLineKey = keylet::trustLine(truster, trustLineIssue);
                     destLow = deliverIssuer > accountID_;
 
                     if (!psb.exists(*trustLineKey))
diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp
index ff82912a1f..595bcc4ab1 100644
--- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp
+++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp
@@ -127,7 +127,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
                     {
                         // Check if the issuer froze the line
                         auto const sleTrust =
-                            ctx.view.read(keylet::line(srcId, issuerId, issue.currency));
+                            ctx.view.read(keylet::trustLine(srcId, issuerId, issue.currency));
                         if (sleTrust &&
                             sleTrust->isFlag((issuerId > srcId) ? lsfHighFreeze : lsfLowFreeze))
                         {
@@ -139,7 +139,7 @@ CheckCreate::preclaim(PreclaimContext const& ctx)
                     {
                         // Check if dst froze the line.
                         auto const sleTrust =
-                            ctx.view.read(keylet::line(issuerId, dstId, issue.currency));
+                            ctx.view.read(keylet::trustLine(issuerId, dstId, issue.currency));
                         if (sleTrust &&
                             sleTrust->isFlag((dstId > issuerId) ? lsfHighFreeze : lsfLowFreeze))
                         {
diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp
index 0cc2be381f..c1ef9f875e 100644
--- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp
@@ -136,7 +136,7 @@ AMMClawback::preclaim(PreclaimContext const& ctx)
                     !sleIssuer->isFlag(lsfNoFreeze);
             },
             [&](MPTIssue const& issue) {
-                auto const sleIssuance = ctx.view.read(keylet::mptIssuance(issue.getMptID()));
+                auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(issue.getMptID()));
 
                 return sleIssuance && sleIssuance->isFlag(lsfMPTCanClawback) &&
                     sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount];
diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp
index 9c1a5cfacb..ef271ba362 100644
--- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp
@@ -212,7 +212,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
     auto clawbackDisabled = [&](Asset const& asset) -> TER {
         return asset.visit(
             [&](MPTIssue const& issue) -> TER {
-                auto const sle = ctx.view.read(keylet::mptIssuance(issue.getMptID()));
+                auto const sle = ctx.view.read(keylet::mptokenIssuance(issue.getMptID()));
                 if (!sle)
                     return tecINTERNAL;  // LCOV_EXCL_LINE
                 if (sle->isFlag(lsfMPTCanClawback))
@@ -260,7 +260,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
 
     // LP Token already exists. (should not happen)
     auto const lptIss = ammLPTIssue(amount.asset(), amount2.asset(), accountId);
-    if (sb.read(keylet::line(accountId, lptIss)))
+    if (sb.read(keylet::trustLine(accountId, lptIss)))
     {
         JLOG(j.error()) << "AMM Instance: LP Token already exists.";
         return {tecDUPLICATE, false};
@@ -330,7 +330,8 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
                 // Set AMM flag on AMM trustline
                 if (!isXRP(amount))
                 {
-                    SLE::pointer const sleRippleState = sb.peek(keylet::line(accountId, issue));
+                    SLE::pointer const sleRippleState =
+                        sb.peek(keylet::trustLine(accountId, issue));
                     if (!sleRippleState)
                     {
                         return tecINTERNAL;  // LCOV_EXCL_LINE
@@ -364,7 +365,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou
                     << lpTokens << " " << amount << " " << amount2;
     auto addOrderBook = [&](Asset const& assetIn, Asset const& assetOut, std::uint64_t uRate) {
         Book const book{assetIn, assetOut, std::nullopt};
-        auto const dir = keylet::quality(keylet::kBook(book), uRate);
+        auto const dir = keylet::quality(keylet::book(book), uRate);
         if (auto const bookExisted = static_cast(sb.read(dir)); !bookExisted)
             ctx.registry.get().getOrderBookDB().addOrderBook(book);
     };
diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
index 653e8c6961..3665850f2d 100644
--- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
@@ -233,7 +233,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
             auto const lpIssue = (*ammSle)[sfLPTokenBalance].get();
             // Adjust the reserve if LP doesn't have LPToken trustline
             auto const sle =
-                ctx.view.read(keylet::line(accountID, lpIssue.account, lpIssue.currency));
+                ctx.view.read(keylet::trustLine(accountID, lpIssue.account, lpIssue.currency));
             if (xrpLiquid(ctx.view, accountID, !sle, ctx.j) >= deposit)
                 return TER(tesSUCCESS);
             if (sle)
@@ -532,7 +532,8 @@ AMMDeposit::deposit(
         {
             auto const& lpIssue = lpTokensDeposit.get();
             // Adjust the reserve if LP doesn't have LPToken trustline
-            auto const sle = view.read(keylet::line(accountID_, lpIssue.account, lpIssue.currency));
+            auto const sle =
+                view.read(keylet::trustLine(accountID_, lpIssue.account, lpIssue.currency));
             if (xrpLiquid(view, accountID_, !sle, j_) >= depositAmount)
                 return tesSUCCESS;
         }
diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
index 17ce1a6b83..6cbcfb1e50 100644
--- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
@@ -615,8 +615,8 @@ AMMWithdraw::withdraw(
         bool const isIssue = asset.holds();
         bool const assetNotExists = [&] {
             if (isIssue)
-                return !view.exists(keylet::line(account, asset.get()));
-            auto const issuanceKey = keylet::mptIssuance(asset.get());
+                return !view.exists(keylet::trustLine(account, asset.get()));
+            auto const issuanceKey = keylet::mptokenIssuance(asset.get());
             mptokenKey = keylet::mptoken(issuanceKey.key, account);
             if (!view.exists(*mptokenKey))
                 return true;
diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
index 547d40e7b8..ed5a22e3db 100644
--- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
+++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
@@ -284,7 +284,7 @@ OfferCreate::checkAcceptAsset(
             auto const& issuer = issue.getIssuer();
             if (issuerAccount->isFlag(lsfRequireAuth))
             {
-                auto const trustLine = view.read(keylet::line(id, issuer, issue.currency));
+                auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency));
 
                 if (!trustLine)
                 {
@@ -308,7 +308,7 @@ OfferCreate::checkAcceptAsset(
                 }
             }
 
-            auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
+            auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency));
 
             if (!trustLine)
             {
@@ -575,7 +575,7 @@ OfferCreate::applyHybrid(
     // if offer is hybrid, need to also place into open offer dir
     Book const book{saTakerPays.asset(), saTakerGets.asset(), std::nullopt};
 
-    auto dir = keylet::quality(keylet::kBook(book), openRate);
+    auto dir = keylet::quality(keylet::book(book), openRate);
     bool const bookExists = sb.exists(dir);
 
     auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) {
@@ -887,7 +887,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
     // Hybrid domain offer - BookDirectory points to domain directory,
     // and AdditionalBooks field stores one entry that points to the open
     // directory
-    auto dir = keylet::quality(keylet::kBook(book), uRate);
+    auto dir = keylet::quality(keylet::book(book), uRate);
     bool const bookExisted = static_cast(sb.peek(dir));
 
     auto setBookDir = [&](SLE::ref sle, std::optional const& maybeDomain) {
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp
index b8f4604d73..55a9133e1e 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp
@@ -72,7 +72,7 @@ escrowCancelPreclaimHelper(
         return tecINTERNAL;  // LCOV_EXCL_LINE
 
     // If the mpt does not exist, return tecOBJECT_NOT_FOUND
-    auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID());
+    auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID());
     auto const sleIssuance = ctx.view.read(issuanceKey);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
index 0a12e2d1bc..0d83885349 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
@@ -208,7 +208,7 @@ escrowCreatePreclaimHelper(
         return tecNO_PERMISSION;
 
     // If the account does not have a trustline to the issuer, return tecNO_LINE
-    auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, issue.currency));
+    auto const sleRippleState = ctx.view.read(keylet::trustLine(account, issuer, issue.currency));
     if (!sleRippleState)
         return tecNO_LINE;
 
@@ -271,7 +271,7 @@ escrowCreatePreclaimHelper(
         return tecNO_PERMISSION;
 
     // If the mpt does not exist, return tecOBJECT_NOT_FOUND
-    auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID());
+    auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID());
     auto const sleIssuance = ctx.view.read(issuanceKey);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
index 4cda867b48..7f1f9ec078 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
@@ -172,7 +172,7 @@ escrowFinishPreclaimHelper(
         return tesSUCCESS;
 
     // If the mpt does not exist, return tecOBJECT_NOT_FOUND
-    auto const issuanceKey = keylet::mptIssuance(amount.get().getMptID());
+    auto const issuanceKey = keylet::mptokenIssuance(amount.get().getMptID());
     auto const sleIssuance = ctx.view.read(issuanceKey);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp
index 041bf73abf..9bb84e878e 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp
@@ -218,7 +218,7 @@ preclaimHelper(
     SLE const& sleIssuer,
     STAmount const& clawAmount)
 {
-    auto const issuanceKey = keylet::mptIssuance(clawAmount.get().getMptID());
+    auto const issuanceKey = keylet::mptokenIssuance(clawAmount.get().getMptID());
     auto const sleIssuance = ctx.view.read(issuanceKey);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
@@ -245,7 +245,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
     auto const brokerID = *findBrokerID;
     auto const amount = tx[~sfAmount];
 
-    auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
+    auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
     if (!sleBroker)
     {
         JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
@@ -344,7 +344,7 @@ LoanBrokerCoverClawback::doApply()
     auto const brokerID = *findBrokerID;
     auto const amount = tx[~sfAmount];
 
-    auto sleBroker = view().peek(keylet::loanbroker(brokerID));
+    auto sleBroker = view().peek(keylet::loanBroker(brokerID));
     if (!sleBroker)
         return tecINTERNAL;  // LCOV_EXCL_LINE
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp
index 537996ba57..76a5493a73 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp
@@ -49,7 +49,7 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
     auto const brokerID = tx[sfLoanBrokerID];
     auto const amount = tx[sfAmount];
 
-    auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
+    auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
     if (!sleBroker)
     {
         JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
@@ -129,7 +129,7 @@ LoanBrokerCoverDeposit::doApply()
     auto const& tx = ctx_.tx;
 
     auto const brokerID = tx[sfLoanBrokerID];
-    auto broker = view().peek(keylet::loanbroker(brokerID));
+    auto broker = view().peek(keylet::loanBroker(brokerID));
     if (!broker)
         return tecINTERNAL;  // LCOV_EXCL_LINE
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
index fea6f3b9cb..426f77cc70 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
@@ -68,7 +68,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
         JLOG(ctx.j.warn()) << "Trying to withdraw into a pseudo-account.";
         return tecPSEUDO_ACCOUNT;
     }
-    auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
+    auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
     if (!sleBroker)
     {
         JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
@@ -180,7 +180,7 @@ LoanBrokerCoverWithdraw::doApply()
     auto const amount = tx[sfAmount];
     auto const dstAcct = tx[~sfDestination].value_or(accountID_);
 
-    auto broker = view().peek(keylet::loanbroker(brokerID));
+    auto broker = view().peek(keylet::loanBroker(brokerID));
     if (!broker)
         return tecINTERNAL;  // LCOV_EXCL_LINE
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp
index f3c000bf0b..141cc2cf56 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp
@@ -43,7 +43,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
     auto const account = tx[sfAccount];
     auto const brokerID = tx[sfLoanBrokerID];
 
-    auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
+    auto const sleBroker = ctx.view.read(keylet::loanBroker(brokerID));
     if (!sleBroker)
     {
         JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
@@ -129,7 +129,7 @@ LoanBrokerDelete::doApply()
     auto const brokerID = tx[sfLoanBrokerID];
 
     // Delete the loan broker
-    auto broker = view().peek(keylet::loanbroker(brokerID));
+    auto broker = view().peek(keylet::loanBroker(brokerID));
     if (!broker)
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
     auto const vaultID = broker->at(sfVaultID);
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp
index 2dc003eb7f..ab1c8f22dc 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp
@@ -114,7 +114,7 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
     {
         // Updating an existing Broker
 
-        auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID));
+        auto const sleBroker = ctx.view.read(keylet::loanBroker(*brokerID));
         if (!sleBroker)
         {
             JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
@@ -178,7 +178,7 @@ LoanBrokerSet::doApply()
     if (auto const brokerID = tx[~sfLoanBrokerID])
     {
         // Modify an existing LoanBroker
-        auto broker = view.peek(keylet::loanbroker(*brokerID));
+        auto broker = view.peek(keylet::loanBroker(*brokerID));
         if (!broker)
         {
             // This should be impossible
@@ -229,7 +229,7 @@ LoanBrokerSet::doApply()
             return tefBAD_LEDGER;
             // LCOV_EXCL_STOP
         }
-        auto broker = std::make_shared(keylet::loanbroker(accountID_, sequence));
+        auto broker = std::make_shared(keylet::loanBroker(accountID_, sequence));
 
         if (auto const ter = dirLink(view, accountID_, broker))
             return ter;  // LCOV_EXCL_LINE
diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp
index d4ec92a9fb..3459119379 100644
--- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp
@@ -54,7 +54,7 @@ LoanDelete::preclaim(PreclaimContext const& ctx)
     }
 
     auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
-    auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
+    auto const loanBrokerSle = ctx.view.read(keylet::loanBroker(loanBrokerID));
     if (!loanBrokerSle)
     {
         // should be impossible
@@ -85,7 +85,7 @@ LoanDelete::doApply()
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
 
     auto const brokerID = loanSle->at(sfLoanBrokerID);
-    auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
+    auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
     if (!brokerSle)
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
     auto const brokerPseudoAccount = brokerSle->at(sfAccount);
diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp
index 2b5c9d25f6..830eb30272 100644
--- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp
@@ -111,7 +111,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
     }
 
     auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
-    auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
+    auto const loanBrokerSle = ctx.view.read(keylet::loanBroker(loanBrokerID));
     if (!loanBrokerSle)
     {
         // should be impossible
@@ -398,7 +398,7 @@ LoanManage::doApply()
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
 
     auto const brokerID = loanSle->at(sfLoanBrokerID);
-    auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
+    auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
     if (!brokerSle)
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp
index 5c0b46de42..fcda2a9fff 100644
--- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp
@@ -110,7 +110,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
         return normalCost;
     }
 
-    auto const brokerSle = view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID)));
+    auto const brokerSle = view.read(keylet::loanBroker(loanSle->at(sfLoanBrokerID)));
     if (!brokerSle)
     {
         // Let preclaim worry about the error for this
@@ -213,7 +213,7 @@ LoanPay::preclaim(PreclaimContext const& ctx)
     }
 
     auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
-    auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
+    auto const loanBrokerSle = ctx.view.read(keylet::loanBroker(loanBrokerID));
     if (!loanBrokerSle)
     {
         // This should be impossible
@@ -293,7 +293,7 @@ LoanPay::doApply()
     std::int32_t const loanScale = loanSle->at(sfLoanScale);
 
     auto const brokerID = loanSle->at(sfLoanBrokerID);
-    auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
+    auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
     if (!brokerSle)
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
     auto const brokerOwner = brokerSle->at(sfOwner);
diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp
index 573f700f51..903707320b 100644
--- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp
@@ -150,7 +150,7 @@ LoanSet::checkSign(PreclaimContext const& ctx)
         if (auto const c = ctx.tx.at(~sfCounterparty))
             return c;
 
-        if (auto const broker = ctx.view.read(keylet::loanbroker(ctx.tx[sfLoanBrokerID])))
+        if (auto const broker = ctx.view.read(keylet::loanBroker(ctx.tx[sfLoanBrokerID])))
             return broker->at(sfOwner);
         return std::nullopt;
     }();
@@ -268,7 +268,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
     auto const account = tx[sfAccount];
     auto const brokerID = tx[sfLoanBrokerID];
 
-    auto const brokerSle = ctx.view.read(keylet::loanbroker(brokerID));
+    auto const brokerSle = ctx.view.read(keylet::loanBroker(brokerID));
     if (!brokerSle)
     {
         // This can only be hit if there's a counterparty specified, otherwise
@@ -373,7 +373,7 @@ LoanSet::doApply()
 
     auto const brokerID = tx[sfLoanBrokerID];
 
-    auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
+    auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
     if (!brokerSle)
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
     auto const brokerOwner = brokerSle->at(sfOwner);
diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
index 6a82a15044..1d303b7141 100644
--- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
+++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
@@ -60,7 +60,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
             if (id->isZero())
                 return {nullptr, tecOBJECT_NOT_FOUND};
 
-            auto offerSLE = ctx.view.read(keylet::nftoffer(*id));
+            auto offerSLE = ctx.view.read(keylet::nftokenOffer(*id));
 
             if (!offerSLE)
                 return {nullptr, tecOBJECT_NOT_FOUND};
@@ -307,7 +307,7 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
         if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) &&
             (nft::getFlags(tokenID) & nft::kFlagCreateTrustLines) == 0 &&
             nftMinter != amount.getIssuer() &&
-            !ctx.view.read(keylet::line(nftMinter, amount.get())))
+            !ctx.view.read(keylet::trustLine(nftMinter, amount.get())))
             return tecNO_LINE;
 
         // Check that the issuer is allowed to receive IOUs.
@@ -442,7 +442,7 @@ NFTokenAcceptOffer::doApply()
     auto const loadToken = [this](std::optional const& id) {
         SLE::pointer sle;
         if (id)
-            sle = view().peek(keylet::nftoffer(*id));
+            sle = view().peek(keylet::nftokenOffer(*id));
         return sle;
     };
 
diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp
index d04714907e..6c6f7d4e8b 100644
--- a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp
+++ b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp
@@ -88,7 +88,7 @@ NFTokenCancelOffer::doApply()
 {
     for (auto const& id : ctx_.tx[sfNFTokenOffers])
     {
-        if (auto offer = view().peek(keylet::nftoffer(id));
+        if (auto offer = view().peek(keylet::nftokenOffer(id));
             offer && !nft::deleteTokenOffer(view(), offer))
         {
             // LCOV_EXCL_START
diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp
index 63dbe01944..977da56861 100644
--- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp
+++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp
@@ -137,7 +137,7 @@ PaymentChannelCreate::doApply()
     //
     // Note that we use the value from the sequence or ticket as the
     // payChan sequence.  For more explanation see comments in SeqProxy.h.
-    Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
+    Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqValue());
     auto const slep = std::make_shared(payChanKeylet);
 
     // Funds held in this channel
diff --git a/src/libxrpl/tx/transactors/system/Change.cpp b/src/libxrpl/tx/transactors/system/Change.cpp
index 38fee5bf92..f27855a5c8 100644
--- a/src/libxrpl/tx/transactors/system/Change.cpp
+++ b/src/libxrpl/tx/transactors/system/Change.cpp
@@ -254,7 +254,7 @@ Change::applyAmendment()
 TER
 Change::applyFee()
 {
-    auto const k = keylet::fees();
+    auto const k = keylet::feeSettings();
 
     SLE::pointer feeObject = view().peek(k);
 
diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp
index be24b6326a..73a72c217d 100644
--- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp
+++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp
@@ -100,7 +100,7 @@ TicketCreate::doApply()
     for (std::uint32_t i = 0; i < ticketCount; ++i)
     {
         std::uint32_t const curTicketSeq = firstTicketSeq + i;
-        Keylet const ticketKeylet = keylet::kTicket(accountID_, curTicketSeq);
+        Keylet const ticketKeylet = keylet::ticket(accountID_, curTicketSeq);
         SLE::pointer const sleTicket = std::make_shared(ticketKeylet);
 
         sleTicket->setAccountID(sfAccount, accountID_);
diff --git a/src/libxrpl/tx/transactors/token/Clawback.cpp b/src/libxrpl/tx/transactors/token/Clawback.cpp
index 06132c1c97..64457a3b5f 100644
--- a/src/libxrpl/tx/transactors/token/Clawback.cpp
+++ b/src/libxrpl/tx/transactors/token/Clawback.cpp
@@ -109,7 +109,7 @@ preclaimHelper(
         return tecNO_PERMISSION;
 
     auto const sleRippleState =
-        ctx.view.read(keylet::line(holder, issuer, clawAmount.get().currency));
+        ctx.view.read(keylet::trustLine(holder, issuer, clawAmount.get().currency));
     if (!sleRippleState)
         return tecNO_LINE;
 
@@ -153,7 +153,7 @@ preclaimHelper(
     AccountID const& holder,
     STAmount const& clawAmount)
 {
-    auto const issuanceKey = keylet::mptIssuance(clawAmount.get().getMptID());
+    auto const issuanceKey = keylet::mptokenIssuance(clawAmount.get().getMptID());
     auto const sleIssuance = ctx.view.read(issuanceKey);
     if (!sleIssuance)
         return tecOBJECT_NOT_FOUND;
diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
index 332d160cac..e1d9daaada 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
@@ -64,7 +64,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
             if ((*sleMpt)[sfMPTAmount] != 0)
             {
                 auto const sleMptIssuance =
-                    ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
+                    ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
                 if (!sleMptIssuance)
                     return tefINTERNAL;  // LCOV_EXCL_LINE
 
@@ -74,7 +74,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
             if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0)
             {
                 auto const sleMptIssuance =
-                    ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
+                    ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
                 if (!sleMptIssuance)
                     return tefINTERNAL;  // LCOV_EXCL_LINE
 
@@ -87,7 +87,8 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
         }
 
         // Now test when the holder wants to hold/create/authorize a new MPT
-        auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
+        auto const sleMptIssuance =
+            ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
 
         if (!sleMptIssuance)
             return tecOBJECT_NOT_FOUND;
@@ -106,7 +107,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
     if (!sleHolder)
         return tecNO_DST;
 
-    auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
+    auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
     if (!sleMptIssuance)
         return tecOBJECT_NOT_FOUND;
 
diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp
index 90e33d3a70..68956a533d 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp
@@ -112,7 +112,7 @@ MPTokenIssuanceCreate::create(ApplyView& view, beast::Journal journal, MPTCreate
         return std::unexpected(tecINSUFFICIENT_RESERVE);
 
     auto const mptId = makeMptID(args.sequence, args.account);
-    auto const mptIssuanceKeylet = keylet::mptIssuance(mptId);
+    auto const mptIssuanceKeylet = keylet::mptokenIssuance(mptId);
 
     // create the MPTokenIssuance
     {
diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp
index 1029c25813..c56697767b 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp
@@ -21,7 +21,7 @@ TER
 MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
 {
     // ensure that issuance exists
-    auto const sleMPT = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
+    auto const sleMPT = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
     if (!sleMPT)
         return tecOBJECT_NOT_FOUND;
 
@@ -42,7 +42,7 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx)
 TER
 MPTokenIssuanceDestroy::doApply()
 {
-    auto const mpt = view().peek(keylet::mptIssuance(ctx_.tx[sfMPTokenIssuanceID]));
+    auto const mpt = view().peek(keylet::mptokenIssuance(ctx_.tx[sfMPTokenIssuanceID]));
     if (accountID_ != mpt->getAccountID(sfIssuer))
         return tecINTERNAL;  // LCOV_EXCL_LINE
 
diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
index 1cb12709d5..e200c9762a 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
@@ -127,7 +127,7 @@ TER
 MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
 {
     // ensure that issuance exists
-    auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
+    auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
     if (!sleMptIssuance)
         return tecOBJECT_NOT_FOUND;
 
@@ -222,7 +222,7 @@ MPTokenIssuanceSet::doApply()
     }
     else
     {
-        sle = view().peek(keylet::mptIssuance(mptIssuanceID));
+        sle = view().peek(keylet::mptokenIssuance(mptIssuanceID));
     }
 
     if (!sle)
diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp
index 7838b212b2..151e82bab9 100644
--- a/src/libxrpl/tx/transactors/token/TrustSet.cpp
+++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp
@@ -129,7 +129,7 @@ TrustSet::checkGranularSemantics(
 {
     auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount);
     auto const sleRippleState = view.read(
-        keylet::line(
+        keylet::trustLine(
             tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get().currency));
 
     // granular permissions are not allowed to create a trustline
@@ -192,7 +192,7 @@ TrustSet::preclaim(PreclaimContext const& ctx)
         //   o The trust line already exists
         // Then allow the TrustSet.
         if (ctx.view.rules().enabled(fixDisallowIncomingV1) &&
-            ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
+            ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency)))
         {
             // pass
         }
@@ -212,7 +212,7 @@ TrustSet::preclaim(PreclaimContext const& ctx)
         // TrustSet if the asset is AMM LP token and AMM is not in empty state.
         if (sleDst->isFieldPresent(sfAMMID))
         {
-            if (ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
+            if (ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency)))
             {
                 // pass
             }
@@ -235,7 +235,7 @@ TrustSet::preclaim(PreclaimContext const& ctx)
         }
         else if (sleDst->isFieldPresent(sfVaultID) || sleDst->isFieldPresent(sfLoanBrokerID))
         {
-            if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
+            if (!ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency)))
                 return tecNO_PERMISSION;
             // else pass
         }
@@ -269,7 +269,7 @@ TrustSet::preclaim(PreclaimContext const& ctx)
 
         bool const bHigh = id > uDstAccountID;
         // Fetching current state of trust line
-        auto const sleRippleState = ctx.view.read(keylet::line(id, uDstAccountID, currency));
+        auto const sleRippleState = ctx.view.read(keylet::trustLine(id, uDstAccountID, currency));
         std::uint32_t uFlags = sleRippleState ? sleRippleState->getFieldU32(sfFlags) : 0u;
         // Computing expected trust line state
         uFlags = computeFreezeFlags(
@@ -361,7 +361,7 @@ TrustSet::doApply()
     saLimitAllow.get().account = accountID_;
 
     SLE::pointer const sleRippleState =
-        view().peek(keylet::line(accountID_, uDstAccountID, currency));
+        view().peek(keylet::trustLine(accountID_, uDstAccountID, currency));
 
     if (sleRippleState)
     {
@@ -609,7 +609,7 @@ TrustSet::doApply()
         // Zero balance in currency.
         STAmount const saBalance(Issue{currency, noAccount()});
 
-        auto const k = keylet::line(accountID_, uDstAccountID, currency);
+        auto const k = keylet::trustLine(accountID_, uDstAccountID, currency);
 
         JLOG(j_.trace()) << "doTrustSet: Creating ripple line: " << to_string(k.key);
 
diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp
index a8587feaeb..541660c98f 100644
--- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp
@@ -86,7 +86,7 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
     auto const holder = ctx.tx[sfHolder];
     auto const maybeAmount = ctx.tx[~sfAmount];
     auto const mptIssuanceID = vault->at(sfShareMPTID);
-    auto const sleShareIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleShareIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleShareIssuance)
     {
         // LCOV_EXCL_START
@@ -180,7 +180,7 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
 
         return vaultAsset.visit(
             [&](MPTIssue const& issue) -> TER {
-                auto const mptIssue = ctx.view.read(keylet::mptIssuance(issue.getMptID()));
+                auto const mptIssue = ctx.view.read(keylet::mptokenIssuance(issue.getMptID()));
                 if (mptIssue == nullptr)
                     return tecOBJECT_NOT_FOUND;
 
@@ -337,7 +337,7 @@ VaultClawback::doApply()
         return tefINTERNAL;  // LCOV_EXCL_LINE
 
     auto const mptIssuanceID = *vault->at(sfShareMPTID);
-    auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
     {
         // LCOV_EXCL_START
diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp
index 4163753014..d262132b6f 100644
--- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp
@@ -194,7 +194,7 @@ VaultCreate::doApply()
             return std::nullopt;
         return asset.holds()
             ? keylet::mptoken(asset.get().getMptID(), pseudoId).key
-            : keylet::line(pseudoId, asset.get()).key;
+            : keylet::trustLine(pseudoId, asset.get()).key;
     }();
     auto const maybeShare = MPTokenIssuanceCreate::create(
         view(),
diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
index 030a7e971c..0550f08b17 100644
--- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
@@ -57,7 +57,7 @@ VaultDelete::preclaim(PreclaimContext const& ctx)
     }
 
     // Verify we can destroy MPTokenIssuance
-    auto const sleMPT = ctx.view.read(keylet::mptIssuance(vault->at(sfShareMPTID)));
+    auto const sleMPT = ctx.view.read(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
 
     if (!sleMPT)
     {
@@ -110,7 +110,7 @@ VaultDelete::doApply()
     // Destroy the share issuance. Do not use MPTokenIssuanceDestroy for this,
     // no special logic needed. First run few checks, duplicated from preclaim.
     auto const shareMPTID = *vault->at(sfShareMPTID);
-    auto const mpt = view().peek(keylet::mptIssuance(shareMPTID));
+    auto const mpt = view().peek(keylet::mptokenIssuance(shareMPTID));
     if (!mpt)
     {
         // LCOV_EXCL_START
diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
index c08d1e957c..b70543d280 100644
--- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
@@ -90,7 +90,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
         // LCOV_EXCL_STOP
     }
 
-    auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
     {
         // LCOV_EXCL_START
@@ -206,7 +206,7 @@ VaultDeposit::doApply()
 
     // Make sure the depositor can hold shares.
     auto const mptIssuanceID = (*vault)[sfShareMPTID];
-    auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
     {
         // LCOV_EXCL_START
diff --git a/src/libxrpl/tx/transactors/vault/VaultSet.cpp b/src/libxrpl/tx/transactors/vault/VaultSet.cpp
index 6d0ade6e52..c71e84858a 100644
--- a/src/libxrpl/tx/transactors/vault/VaultSet.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultSet.cpp
@@ -75,7 +75,7 @@ VaultSet::preclaim(PreclaimContext const& ctx)
     }
 
     auto const mptIssuanceID = (*vault)[sfShareMPTID];
-    auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
     {
         // LCOV_EXCL_START
@@ -130,7 +130,7 @@ VaultSet::doApply()
     auto const vaultAsset = vault->at(sfAsset);
 
     auto const mptIssuanceID = (*vault)[sfShareMPTID];
-    auto const sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
     {
         // LCOV_EXCL_START
diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
index 05dcfea506..815d8ccd5b 100644
--- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
@@ -106,7 +106,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
         // to the equivalent asset amount before checking withdrawal
         // limits. Pre-amendment the limit check was skipped for
         // share-denominated withdrawals.
-        auto const sleIssuance = ctx.view.read(keylet::mptIssuance(vaultShare));
+        auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(vaultShare));
         if (!sleIssuance)
         {
             // LCOV_EXCL_START
@@ -180,7 +180,7 @@ VaultWithdraw::doApply()
         return tefINTERNAL;  // LCOV_EXCL_LINE
 
     auto const mptIssuanceID = *((*vault)[sfShareMPTID]);
-    auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
+    auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
     {
         // LCOV_EXCL_START
diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp
index 65ff9ed839..399696ec0d 100644
--- a/src/test/app/AccountDelete_test.cpp
+++ b/src/test/app/AccountDelete_test.cpp
@@ -215,8 +215,8 @@ public:
             BEAST_EXPECT(env.closed()->exists(keylet::ownerDir(carol.id())));
             BEAST_EXPECT(env.closed()->exists(keylet::depositPreauth(carol.id(), becky.id())));
             BEAST_EXPECT(env.closed()->exists(keylet::offer(carol.id(), carolOfferSeq)));
-            BEAST_EXPECT(env.closed()->exists(keylet::kTicket(carol.id(), carolTicketSeq)));
-            BEAST_EXPECT(env.closed()->exists(keylet::signers(carol.id())));
+            BEAST_EXPECT(env.closed()->exists(keylet::ticket(carol.id(), carolTicketSeq)));
+            BEAST_EXPECT(env.closed()->exists(keylet::signerList(carol.id())));
 
             // Delete carol's account even with stuff in her directory.  Show
             // that multisigning for the delete does not increase carol's fee.
@@ -229,8 +229,8 @@ public:
             BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(carol.id())));
             BEAST_EXPECT(!env.closed()->exists(keylet::depositPreauth(carol.id(), becky.id())));
             BEAST_EXPECT(!env.closed()->exists(keylet::offer(carol.id(), carolOfferSeq)));
-            BEAST_EXPECT(!env.closed()->exists(keylet::kTicket(carol.id(), carolTicketSeq)));
-            BEAST_EXPECT(!env.closed()->exists(keylet::signers(carol.id())));
+            BEAST_EXPECT(!env.closed()->exists(keylet::ticket(carol.id(), carolTicketSeq)));
+            BEAST_EXPECT(!env.closed()->exists(keylet::signerList(carol.id())));
 
             // Verify that Carol's XRP, minus the fee, was transferred to becky.
             BEAST_EXPECT(env.balance(becky) == carolOldBalance + beckyOldBalance - acctDelFee);
@@ -386,7 +386,7 @@ public:
         env(escrow::cancel(becky, alice, escrowSeq));
         env.close();
 
-        Keylet const alicePayChanKey{keylet::payChan(alice, becky, env.seq(alice))};
+        Keylet const alicePayChanKey{keylet::payChannel(alice, becky, env.seq(alice))};
 
         env(payChanCreate(alice, becky, XRP(57), 4s, env.now() + 2s, alice.pk()));
         env.close();
@@ -417,7 +417,7 @@ public:
 
         // gw creates a PayChannel with alice as the destination, this should
         // prevent alice from deleting her account.
-        Keylet const gwPayChanKey{keylet::payChan(gw, alice, env.seq(gw))};
+        Keylet const gwPayChanKey{keylet::payChannel(gw, alice, env.seq(gw))};
 
         env(payChanCreate(gw, alice, XRP(68), 4s, env.now() + 2s, alice.pk()));
         env.close();
@@ -662,7 +662,7 @@ public:
             BEAST_EXPECT(closed->exists(keylet::account(bob.id())));
             for (std::uint32_t i = 0; i < 250; ++i)
             {
-                BEAST_EXPECT(closed->exists(keylet::kTicket(bob.id(), ticketSeq + i)));
+                BEAST_EXPECT(closed->exists(keylet::ticket(bob.id(), ticketSeq + i)));
             }
         }
 
@@ -681,7 +681,7 @@ public:
             BEAST_EXPECT(!closed->exists(keylet::account(bob.id())));
             for (std::uint32_t i = 0; i < 250; ++i)
             {
-                BEAST_EXPECT(!closed->exists(keylet::kTicket(bob.id(), ticketSeq + i)));
+                BEAST_EXPECT(!closed->exists(keylet::ticket(bob.id(), ticketSeq + i)));
             }
         }
     }
diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp
index 47f9a84fb9..6aaae62155 100644
--- a/src/test/app/Batch_test.cpp
+++ b/src/test/app/Batch_test.cpp
@@ -3040,7 +3040,7 @@ class Batch_test : public beast::unit_test::Suite
         env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
         env.close();
 
-        auto const brokerKeylet = keylet::loanbroker(lender.id(), env.seq(lender));
+        auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender));
 
         {
             using namespace loanBroker;
diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp
index 9b814a24b1..3ad1e3fa4e 100644
--- a/src/test/app/Check_test.cpp
+++ b/src/test/app/Check_test.cpp
@@ -1951,8 +1951,8 @@ class Check_test : public beast::unit_test::Suite
                                  Account const& acct2,
                                  IOU const& offerIou,
                                  IOU const& checkIou) {
-            auto const offerLine = env.le(keylet::line(acct1, acct2, offerIou.currency));
-            auto const checkLine = env.le(keylet::line(acct1, acct2, checkIou.currency));
+            auto const offerLine = env.le(keylet::trustLine(acct1, acct2, offerIou.currency));
+            auto const checkLine = env.le(keylet::trustLine(acct1, acct2, checkIou.currency));
             if (offerLine == nullptr || checkLine == nullptr)
             {
                 BEAST_EXPECT(offerLine == nullptr && checkLine == nullptr);
@@ -2023,7 +2023,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF1 = gw1["OF1"];
             env(offer(gw1, XRP(98), oF1(98)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, oF1.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, oF1.currency)) == nullptr);
             env(offer(alice, oF1(98), XRP(98)));
             ++alice.owners;
             env.close();
@@ -2041,7 +2041,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))};
             env(check::create(gw1, alice, cK1(98)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, cK1.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, cK1.currency)) == nullptr);
             env(check::cash(alice, chkId, cK1(98)));
             ++alice.owners;
             verifyDeliveredAmount(env, cK1(98));
@@ -2070,7 +2070,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF1 = gw1["OF1"];
             env(offer(alice, XRP(97), oF1(97)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, oF1.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, oF1.currency)) == nullptr);
             env(offer(bob, oF1(97), XRP(97)));
             ++bob.owners;
             env.close();
@@ -2094,12 +2094,12 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
             env(check::create(alice, bob, cK1(97)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, cK1.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, cK1.currency)) == nullptr);
             env(check::cash(bob, chkId, cK1(97)), Ter(terNO_RIPPLE));
             env.close();
 
-            BEAST_EXPECT(env.le(keylet::line(gw1, bob, oF1.currency)) != nullptr);
-            BEAST_EXPECT(env.le(keylet::line(gw1, bob, cK1.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, bob, oF1.currency)) != nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, bob, cK1.currency)) == nullptr);
 
             // Delete alice's check since it is no longer needed.
             env(check::cancel(alice, chkId));
@@ -2123,7 +2123,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF2 = gw1["OF2"];
             env(offer(gw1, XRP(96), oF2(96)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, oF2.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, oF2.currency)) == nullptr);
             env(offer(alice, oF2(96), XRP(96)));
             ++alice.owners;
             env.close();
@@ -2141,7 +2141,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))};
             env(check::create(gw1, alice, cK2(96)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, cK2.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, cK2.currency)) == nullptr);
             env(check::cash(alice, chkId, cK2(96)));
             ++alice.owners;
             verifyDeliveredAmount(env, cK2(96));
@@ -2167,7 +2167,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF2 = gw1["OF2"];
             env(offer(alice, XRP(95), oF2(95)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, oF2.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, oF2.currency)) == nullptr);
             env(offer(bob, oF2(95), XRP(95)));
             ++bob.owners;
             env.close();
@@ -2182,7 +2182,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
             env(check::create(alice, bob, cK2(95)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, cK2.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, cK2.currency)) == nullptr);
             env(check::cash(bob, chkId, cK2(95)));
             ++bob.owners;
             verifyDeliveredAmount(env, cK2(95));
@@ -2214,7 +2214,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF3 = gw1["OF3"];
             env(offer(gw1, XRP(94), oF3(94)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, oF3.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, oF3.currency)) == nullptr);
             env(offer(alice, oF3(94), XRP(94)));
             ++alice.owners;
             env.close();
@@ -2232,7 +2232,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))};
             env(check::create(gw1, alice, cK3(94)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, cK3.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, cK3.currency)) == nullptr);
             env(check::cash(alice, chkId, cK3(94)));
             ++alice.owners;
             verifyDeliveredAmount(env, cK3(94));
@@ -2258,7 +2258,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF3 = gw1["OF3"];
             env(offer(alice, XRP(93), oF3(93)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, oF3.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, oF3.currency)) == nullptr);
             env(offer(bob, oF3(93), XRP(93)));
             ++bob.owners;
             env.close();
@@ -2273,7 +2273,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
             env(check::create(alice, bob, cK3(93)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, cK3.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, cK3.currency)) == nullptr);
             env(check::cash(bob, chkId, cK3(93)));
             ++bob.owners;
             verifyDeliveredAmount(env, cK3(93));
@@ -2299,7 +2299,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF4 = gw1["OF4"];
             env(offer(gw1, XRP(92), oF4(92)), Ter(tecFROZEN));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, oF4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, oF4.currency)) == nullptr);
             env(offer(alice, oF4(92), XRP(92)), Ter(tecFROZEN));
             env.close();
 
@@ -2313,7 +2313,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(gw1, env.seq(gw1))};
             env(check::create(gw1, alice, cK4(92)), Ter(tecFROZEN));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, cK4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, cK4.currency)) == nullptr);
             env(check::cash(alice, chkId, cK4(92)), Ter(tecNO_ENTRY));
             env.close();
 
@@ -2324,8 +2324,8 @@ class Check_test : public beast::unit_test::Suite
 
             // Because gw1 has set lsfGlobalFreeze, neither trust line
             // is created.
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, oF4.currency)) == nullptr);
-            BEAST_EXPECT(env.le(keylet::line(gw1, alice, cK4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, oF4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, alice, cK4.currency)) == nullptr);
         }
         //------------ lsfGlobalFreeze, check written by non-issuer ------------
         {
@@ -2337,7 +2337,7 @@ class Check_test : public beast::unit_test::Suite
             IOU const oF4 = gw1["OF4"];
             env(offer(alice, XRP(91), oF4(91)), Ter(tecFROZEN));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, oF4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, oF4.currency)) == nullptr);
             env(offer(bob, oF4(91), XRP(91)), Ter(tecFROZEN));
             env.close();
 
@@ -2351,7 +2351,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
             env(check::create(alice, bob, cK4(91)), Ter(tecFROZEN));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, cK4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, cK4.currency)) == nullptr);
             env(check::cash(bob, chkId, cK4(91)), Ter(tecNO_ENTRY));
             env.close();
 
@@ -2362,8 +2362,8 @@ class Check_test : public beast::unit_test::Suite
 
             // Because gw1 has set lsfGlobalFreeze, neither trust line
             // is created.
-            BEAST_EXPECT(env.le(keylet::line(gw1, bob, oF4.currency)) == nullptr);
-            BEAST_EXPECT(env.le(keylet::line(gw1, bob, cK4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, bob, oF4.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw1, bob, cK4.currency)) == nullptr);
         }
 
         //-------------- lsfRequireAuth, check written by issuer ---------------
@@ -2387,7 +2387,7 @@ class Check_test : public beast::unit_test::Suite
             env(offer(gw2, XRP(92), oF5(92)));
             ++gw2.owners;
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw2, alice, oF5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, alice, oF5.currency)) == nullptr);
             env(offer(alice, oF5(92), XRP(92)), Ter(tecNO_LINE));
             env.close();
 
@@ -2409,7 +2409,7 @@ class Check_test : public beast::unit_test::Suite
             env(check::create(gw2, alice, cK5(92)));
             ++gw2.owners;
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(gw2, alice, cK5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, alice, cK5.currency)) == nullptr);
             env(check::cash(alice, chkId, cK5(92)), Ter(tecNO_AUTH));
             env.close();
 
@@ -2421,8 +2421,8 @@ class Check_test : public beast::unit_test::Suite
 
             // Because gw2 has set lsfRequireAuth, neither trust line
             // is created.
-            BEAST_EXPECT(env.le(keylet::line(gw2, alice, oF5.currency)) == nullptr);
-            BEAST_EXPECT(env.le(keylet::line(gw2, alice, cK5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, alice, oF5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, alice, cK5.currency)) == nullptr);
 
             // Since we don't need it any more, remove gw2's check.
             env(check::cancel(gw2, chkId));
@@ -2441,7 +2441,7 @@ class Check_test : public beast::unit_test::Suite
             env(offer(alice, XRP(91), oF5(91)), Ter(tecUNFUNDED_OFFER));
             env.close();
             env(offer(bob, oF5(91), XRP(91)), Ter(tecNO_LINE));
-            BEAST_EXPECT(env.le(keylet::line(gw2, bob, oF5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, bob, oF5.currency)) == nullptr);
             env.close();
 
             gw2.verifyOwners(__LINE__);
@@ -2453,7 +2453,7 @@ class Check_test : public beast::unit_test::Suite
             uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
             env(check::create(alice, bob, cK5(91)));
             env.close();
-            BEAST_EXPECT(env.le(keylet::line(alice, bob, cK5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(alice, bob, cK5.currency)) == nullptr);
             env(check::cash(bob, chkId, cK5(91)), Ter(tecPATH_PARTIAL));
             env.close();
 
@@ -2468,8 +2468,8 @@ class Check_test : public beast::unit_test::Suite
 
             // Because gw2 has set lsfRequireAuth, neither trust line
             // is created.
-            BEAST_EXPECT(env.le(keylet::line(gw2, bob, oF5.currency)) == nullptr);
-            BEAST_EXPECT(env.le(keylet::line(gw2, bob, cK5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, bob, oF5.currency)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(gw2, bob, cK5.currency)) == nullptr);
         }
     }
 
diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp
index 5d0ecf022d..b1a756382d 100644
--- a/src/test/app/Clawback_test.cpp
+++ b/src/test/app/Clawback_test.cpp
@@ -53,7 +53,7 @@ class Clawback_test : public beast::unit_test::Suite
         test::jtx::Account const& dst,
         Currency const& cur)
     {
-        if (auto sle = env.le(keylet::line(src, dst, cur)))
+        if (auto sle = env.le(keylet::trustLine(src, dst, cur)))
         {
             auto const useHigh = src.id() > dst.id();
             return sle->isFlag(useHigh ? lsfHighFreeze : lsfLowFreeze);
diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp
index 5bb1303dba..666ee498bd 100644
--- a/src/test/app/EscrowToken_test.cpp
+++ b/src/test/app/EscrowToken_test.cpp
@@ -57,7 +57,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
     static uint64_t
     issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
     {
-        auto const sle = env.le(keylet::mptIssuance(mpt.mpt()));
+        auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt()));
         if (sle && sle->isFieldPresent(sfLockedAmount))
             return (*sle)[sfLockedAmount];
         return 0;
@@ -929,7 +929,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
             env(trust(alice, usd(0)));
             env.close();
 
-            auto const trustLineKey = keylet::line(alice.id(), gw.id(), usd.currency);
+            auto const trustLineKey = keylet::trustLine(alice.id(), gw.id(), usd.currency);
             BEAST_EXPECT(!env.current()->exists(trustLineKey));
 
             env.close();
@@ -3177,7 +3177,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
             BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
             BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
             BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
-            BEAST_EXPECT(!env.le(keylet::mptIssuance(mpt.mpt()))->isFieldPresent(sfLockedAmount));
+            BEAST_EXPECT(
+                !env.le(keylet::mptokenIssuance(mpt.mpt()))->isFieldPresent(sfLockedAmount));
         }
 
         // Max MPT Amount Issued (Escrow Max MPT)
diff --git a/src/test/app/FeeVote_test.cpp b/src/test/app/FeeVote_test.cpp
index bf42e762c6..666dec1249 100644
--- a/src/test/app/FeeVote_test.cpp
+++ b/src/test/app/FeeVote_test.cpp
@@ -144,7 +144,7 @@ verifyFeeObject(
     Rules const& rules,
     FeeSettingsFields const& expected)
 {
-    auto const feeObject = ledger->read(keylet::fees());
+    auto const feeObject = ledger->read(keylet::feeSettings());
     if (!feeObject)
         return false;
 
diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp
index cfe2fd1808..7b13fc060b 100644
--- a/src/test/app/FixNFTokenPageLinks_test.cpp
+++ b/src/test/app/FixNFTokenPageLinks_test.cpp
@@ -267,7 +267,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         // Get the index of the middle page.
         uint256 const aliceMiddleNFTokenPageIndex = [&env, &alice]() {
-            auto lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            auto lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             return lastNFTokenPage->at(sfPreviousPageMin);
         }();
 
@@ -290,12 +290,12 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // Removing the last token from the last page deletes the last
         // page.  This is a bug.  The contents of the next-to-last page
         // should have been moved into the last page.
-        BEAST_EXPECT(!env.le(keylet::nftpageMax(alice)));
+        BEAST_EXPECT(!env.le(keylet::nftokenPageMax(alice)));
 
         // alice's "middle" page is still present, but has no links.
         {
-            auto aliceMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), aliceMiddleNFTokenPageIndex));
+            auto aliceMiddleNFTokenPage = env.le(
+                keylet::nftokenPage(keylet::nftokenPageMin(alice), aliceMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(aliceMiddleNFTokenPage))
                 return;
 
@@ -315,7 +315,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         // Get the index of the middle page.
         uint256 const bobMiddleNFTokenPageIndex = [&env, &bob]() {
-            auto lastNFTokenPage = env.le(keylet::nftpageMax(bob));
+            auto lastNFTokenPage = env.le(keylet::nftokenPageMax(bob));
             return lastNFTokenPage->at(sfPreviousPageMin);
         }();
 
@@ -332,13 +332,13 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // Removing the last token from the last page deletes the last
         // page.  This is a bug.  The contents of the next-to-last page
         // should have been moved into the last page.
-        BEAST_EXPECT(!env.le(keylet::nftpageMax(bob)));
+        BEAST_EXPECT(!env.le(keylet::nftokenPageMax(bob)));
 
         // bob's "middle" page is still present, but has lost the
         // NextPageMin field.
         {
             auto bobMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(bob), bobMiddleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(bob), bobMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(bobMiddleNFTokenPage))
                 return;
 
@@ -358,7 +358,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         // Get the index of the middle page.
         uint256 const carolMiddleNFTokenPageIndex = [&env, &carol]() {
-            auto lastNFTokenPage = env.le(keylet::nftpageMax(carol));
+            auto lastNFTokenPage = env.le(keylet::nftokenPageMax(carol));
             return lastNFTokenPage->at(sfPreviousPageMin);
         }();
 
@@ -367,7 +367,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         dariaNFTs.reserve(32);
         for (int i = 0; i < 32; ++i)
         {
-            uint256 const offerIndex = keylet::nftoffer(carol, env.seq(carol)).key;
+            uint256 const offerIndex = keylet::nftokenOffer(carol, env.seq(carol)).key;
             env(token::createOffer(carol, carolNFTs.back(), XRP(0)), Txflags(tfSellNFToken));
             env.close();
 
@@ -383,12 +383,12 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // Removing the last token from the last page deletes the last
         // page.  This is a bug.  The contents of the next-to-last page
         // should have been moved into the last page.
-        BEAST_EXPECT(!env.le(keylet::nftpageMax(carol)));
+        BEAST_EXPECT(!env.le(keylet::nftokenPageMax(carol)));
 
         // carol's "middle" page is still present, but has lost the
         // NextPageMin field.
         auto carolMiddleNFTokenPage =
-            env.le(keylet::nftpage(keylet::nftpageMin(carol), carolMiddleNFTokenPageIndex));
+            env.le(keylet::nftokenPage(keylet::nftokenPageMin(carol), carolMiddleNFTokenPageIndex));
         if (!BEAST_EXPECT(carolMiddleNFTokenPage))
             return;
 
@@ -401,7 +401,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // back from daria.
         for (uint256 const& nft : dariaNFTs)
         {
-            uint256 const offerIndex = keylet::nftoffer(carol, env.seq(carol)).key;
+            uint256 const offerIndex = keylet::nftokenOffer(carol, env.seq(carol)).key;
             env(token::createOffer(carol, nft, drops(1)), token::Owner(daria));
             env.close();
 
@@ -418,8 +418,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         // carol's "middle" page is present and still has no NextPageMin field.
         {
-            auto carolMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(carol), carolMiddleNFTokenPageIndex));
+            auto carolMiddleNFTokenPage = env.le(
+                keylet::nftokenPage(keylet::nftokenPageMin(carol), carolMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(carolMiddleNFTokenPage))
                 return;
 
@@ -428,7 +428,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         }
         // carol has a "last" page again, but it has no PreviousPageMin field.
         {
-            auto carolLastNFTokenPage = env.le(keylet::nftpageMax(carol));
+            auto carolLastNFTokenPage = env.le(keylet::nftokenPageMax(carol));
 
             BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
             BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
@@ -456,12 +456,12 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // Verify that alice's NFToken directory is still damaged.
 
         // alice's last page should still be missing.
-        BEAST_EXPECT(!env.le(keylet::nftpageMax(alice)));
+        BEAST_EXPECT(!env.le(keylet::nftokenPageMax(alice)));
 
         // alice's "middle" page is still present and has no links.
         {
-            auto aliceMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), aliceMiddleNFTokenPageIndex));
+            auto aliceMiddleNFTokenPage = env.le(
+                keylet::nftokenPage(keylet::nftokenPageMin(alice), aliceMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(aliceMiddleNFTokenPage))
                 return;
 
@@ -480,7 +480,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         // alice's last page should now be present and include no links.
         {
-            auto aliceLastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            auto aliceLastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             if (!BEAST_EXPECT(aliceLastNFTokenPage))
                 return;
 
@@ -489,8 +489,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         }
 
         // alice's middle page should be gone.
-        BEAST_EXPECT(
-            !env.le(keylet::nftpage(keylet::nftpageMin(alice), aliceMiddleNFTokenPageIndex)));
+        BEAST_EXPECT(!env.le(
+            keylet::nftokenPage(keylet::nftokenPageMin(alice), aliceMiddleNFTokenPageIndex)));
 
         BEAST_EXPECT(nftCount(env, alice) == 32);
         BEAST_EXPECT(ownerCount(env, alice) == 1);
@@ -502,12 +502,12 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // Verify that bob's NFToken directory is still damaged.
 
         // bob's last page should still be missing.
-        BEAST_EXPECT(!env.le(keylet::nftpageMax(bob)));
+        BEAST_EXPECT(!env.le(keylet::nftokenPageMax(bob)));
 
         // bob's "middle" page is still present and missing NextPageMin.
         {
             auto bobMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(bob), bobMiddleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(bob), bobMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(bobMiddleNFTokenPage))
                 return;
 
@@ -522,7 +522,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         // bob's last page should now be present and include a previous
         // link but no next link.
         {
-            auto const lastPageKeylet = keylet::nftpageMax(bob);
+            auto const lastPageKeylet = keylet::nftokenPageMax(bob);
             auto const bobLastNFTokenPage = env.le(lastPageKeylet);
             if (!BEAST_EXPECT(bobLastNFTokenPage))
                 return;
@@ -532,8 +532,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
             BEAST_EXPECT(!bobLastNFTokenPage->isFieldPresent(sfNextPageMin));
 
             auto const bobNewFirstNFTokenPage = env.le(
-                keylet::nftpage(
-                    keylet::nftpageMin(bob), bobLastNFTokenPage->at(sfPreviousPageMin)));
+                keylet::nftokenPage(
+                    keylet::nftokenPageMin(bob), bobLastNFTokenPage->at(sfPreviousPageMin)));
             if (!BEAST_EXPECT(bobNewFirstNFTokenPage))
                 return;
 
@@ -544,7 +544,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
         }
 
         // bob's middle page should be gone.
-        BEAST_EXPECT(!env.le(keylet::nftpage(keylet::nftpageMin(bob), bobMiddleNFTokenPageIndex)));
+        BEAST_EXPECT(
+            !env.le(keylet::nftokenPage(keylet::nftokenPageMin(bob), bobMiddleNFTokenPageIndex)));
 
         BEAST_EXPECT(nftCount(env, bob) == 64);
         BEAST_EXPECT(ownerCount(env, bob) == 2);
@@ -557,17 +558,16 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         // carol's "middle" page is present and has no NextPageMin field.
         {
-            auto carolMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(carol), carolMiddleNFTokenPageIndex));
+            auto carolMiddleNFTokenPage = env.le(
+                keylet::nftokenPage(keylet::nftokenPageMin(carol), carolMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(carolMiddleNFTokenPage))
                 return;
-
             BEAST_EXPECT(carolMiddleNFTokenPage->isFieldPresent(sfPreviousPageMin));
             BEAST_EXPECT(!carolMiddleNFTokenPage->isFieldPresent(sfNextPageMin));
         }
         // carol has a "last" page, but it has no PreviousPageMin field.
         {
-            auto carolLastNFTokenPage = env.le(keylet::nftpageMax(carol));
+            auto carolLastNFTokenPage = env.le(keylet::nftokenPageMax(carol));
 
             BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfPreviousPageMin));
             BEAST_EXPECT(!carolLastNFTokenPage->isFieldPresent(sfNextPageMin));
@@ -579,9 +579,9 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
         {
             // carol's "middle" page is present and now has a NextPageMin field.
-            auto const lastPageKeylet = keylet::nftpageMax(carol);
-            auto carolMiddleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(carol), carolMiddleNFTokenPageIndex));
+            auto const lastPageKeylet = keylet::nftokenPageMax(carol);
+            auto carolMiddleNFTokenPage = env.le(
+                keylet::nftokenPage(keylet::nftokenPageMin(carol), carolMiddleNFTokenPageIndex));
             if (!BEAST_EXPECT(carolMiddleNFTokenPage))
                 return;
 
@@ -602,8 +602,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite
 
             // carol also has a "first" page that includes a NextPageMin field.
             auto carolFirstNFTokenPage = env.le(
-                keylet::nftpage(
-                    keylet::nftpageMin(carol), carolMiddleNFTokenPage->at(sfPreviousPageMin)));
+                keylet::nftokenPage(
+                    keylet::nftokenPageMin(carol), carolMiddleNFTokenPage->at(sfPreviousPageMin)));
             if (!BEAST_EXPECT(carolFirstNFTokenPage))
                 return;
 
diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp
index 5f56a0ceb1..72e39ab890 100644
--- a/src/test/app/Flow_test.cpp
+++ b/src/test/app/Flow_test.cpp
@@ -57,7 +57,7 @@ getNoRippleFlag(
     jtx::Account const& dst,
     Currency const& cur)
 {
-    if (auto sle = env.le(keylet::line(src, dst, cur)))
+    if (auto sle = env.le(keylet::trustLine(src, dst, cur)))
     {
         auto const flag = (src.id() > dst.id()) ? lsfHighNoRipple : lsfLowNoRipple;
         return sle->isFlag(flag);
diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp
index 2b5ca831da..786f5b4680 100644
--- a/src/test/app/Freeze_test.cpp
+++ b/src/test/app/Freeze_test.cpp
@@ -1788,7 +1788,7 @@ class Freeze_test : public beast::unit_test::Suite
             env(token::mint(a2, 0), Txflags(tfTransferable));
             env.close();
 
-            auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+            auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
             env(token::createOffer(a1, nftID, usd(10)), token::Owner(a2));
             env.close();
 
@@ -1874,10 +1874,10 @@ class Freeze_test : public beast::unit_test::Suite
             env(token::mint(a2, 0), Txflags(tfTransferable));
             env.close();
 
-            uint256 const sellIdx = keylet::nftoffer(a2, env.seq(a2)).key;
+            uint256 const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key;
             env(token::createOffer(a2, nftID, usd(10)), Txflags(tfSellNFToken));
             env.close();
-            auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+            auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
             env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2));
             env.close();
 
@@ -1900,13 +1900,13 @@ class Freeze_test : public beast::unit_test::Suite
             env(token::mint(minter, 0), token::XferFee(1u), Txflags(tfTransferable));
             env.close();
 
-            uint256 const minterSellIdx = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellIdx = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, drops(1)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(a2, minterSellIdx));
             env.close();
 
-            uint256 const sellIdx = keylet::nftoffer(a2, env.seq(a2)).key;
+            uint256 const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key;
             env(token::createOffer(a2, nftID, usd(100)), Txflags(tfSellNFToken));
             env.close();
             env(trust(g1, minter["USD"](1000), tfSetFreeze | tfSetDeepFreeze));
@@ -1960,7 +1960,7 @@ class Freeze_test : public beast::unit_test::Suite
         env(token::mint(account, 0), Txflags(tfTransferable));
         env.close();
 
-        uint256 const sellOfferIndex = keylet::nftoffer(account, env.seq(account)).key;
+        uint256 const sellOfferIndex = keylet::nftokenOffer(account, env.seq(account)).key;
         env(token::createOffer(account, nftID, currency), Txflags(tfSellNFToken));
         env.close();
 
diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp
index e0c29ea72a..804cfaebfc 100644
--- a/src/test/app/Invariants_test.cpp
+++ b/src/test/app/Invariants_test.cpp
@@ -462,7 +462,7 @@ class Invariants_test : public beast::unit_test::Suite
                 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
 
                 for (auto const& trustKeylet :
-                     {keylet::line(ammAcctID, a1["USD"]), keylet::line(a1, ammIssue)})
+                     {keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
                 {
                     auto const line = ac.view().peek(trustKeylet);
                     if (!line)
@@ -563,7 +563,7 @@ class Invariants_test : public beast::unit_test::Suite
             [](Account const& a1, Account const& a2, ApplyContext& ac) {
                 // create simple trust SLE with xrp currency
                 auto const sleNew =
-                    std::make_shared(keylet::line(a1, a2, xrpIssue().currency));
+                    std::make_shared(keylet::trustLine(a1, a2, xrpIssue().currency));
                 ac.view().insert(sleNew);
                 return true;
             });
@@ -579,7 +579,8 @@ class Invariants_test : public beast::unit_test::Suite
             {{"a trust line with deep freeze flag without normal freeze was "
               "created"}},
             [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew = std::make_shared(keylet::line(a1, a2, a1["USD"].currency));
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
                 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
                 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
 
@@ -594,7 +595,8 @@ class Invariants_test : public beast::unit_test::Suite
             {{"a trust line with deep freeze flag without normal freeze was "
               "created"}},
             [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew = std::make_shared(keylet::line(a1, a2, a1["USD"].currency));
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
                 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
                 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
                 std::uint32_t uFlags = 0u;
@@ -608,7 +610,8 @@ class Invariants_test : public beast::unit_test::Suite
             {{"a trust line with deep freeze flag without normal freeze was "
               "created"}},
             [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew = std::make_shared(keylet::line(a1, a2, a1["USD"].currency));
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
                 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
                 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
                 std::uint32_t uFlags = 0u;
@@ -622,7 +625,8 @@ class Invariants_test : public beast::unit_test::Suite
             {{"a trust line with deep freeze flag without normal freeze was "
               "created"}},
             [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew = std::make_shared(keylet::line(a1, a2, a1["USD"].currency));
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
                 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
                 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
                 std::uint32_t uFlags = 0u;
@@ -636,7 +640,8 @@ class Invariants_test : public beast::unit_test::Suite
             {{"a trust line with deep freeze flag without normal freeze was "
               "created"}},
             [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew = std::make_shared(keylet::line(a1, a2, a1["USD"].currency));
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
                 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
                 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
                 std::uint32_t uFlags = 0u;
@@ -691,8 +696,8 @@ class Invariants_test : public beast::unit_test::Suite
                                         ApplyContext& ac,
                                         int a1Balance,
                                         int a2Balance) {
-            auto const sleA1 = ac.view().peek(keylet::line(a1, g1["USD"]));
-            auto const sleA2 = ac.view().peek(keylet::line(a2, g1["USD"]));
+            auto const sleA1 = ac.view().peek(keylet::trustLine(a1, g1["USD"]));
+            auto const sleA2 = ac.view().peek(keylet::trustLine(a2, g1["USD"]));
 
             sleA1->setFieldAmount(sfBalance, g1["USD"](a1Balance));
             sleA2->setFieldAmount(sfBalance, g1["USD"](a2Balance));
@@ -961,7 +966,7 @@ class Invariants_test : public beast::unit_test::Suite
                     return false;
 
                 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 sleNew->setFieldU64(sfOutstandingAmount, -1);
                 ac.view().insert(sleNew);
                 return true;
@@ -977,7 +982,7 @@ class Invariants_test : public beast::unit_test::Suite
                     return false;
 
                 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 sleNew->setFieldU64(sfLockedAmount, -1);
                 ac.view().insert(sleNew);
                 return true;
@@ -993,7 +998,7 @@ class Invariants_test : public beast::unit_test::Suite
                     return false;
 
                 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 sleNew->setFieldU64(sfOutstandingAmount, 1);
                 sleNew->setFieldU64(sfLockedAmount, 10);
                 ac.view().insert(sleNew);
@@ -1176,7 +1181,7 @@ class Invariants_test : public beast::unit_test::Suite
         doInvariantCheck(
             {{"NFT page has invalid size"}},
             [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(0));
 
                 ac.view().insert(nftPage);
@@ -1186,7 +1191,7 @@ class Invariants_test : public beast::unit_test::Suite
         doInvariantCheck(
             {{"NFT page has invalid size"}},
             [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(33));
 
                 ac.view().insert(nftPage);
@@ -1199,7 +1204,7 @@ class Invariants_test : public beast::unit_test::Suite
                 STArray nfTokens = makeNFTokenIDs(2);
                 std::iter_swap(nfTokens.begin(), nfTokens.begin() + 1);
 
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, nfTokens);
 
                 ac.view().insert(nftPage);
@@ -1212,7 +1217,7 @@ class Invariants_test : public beast::unit_test::Suite
                 STArray nfTokens = makeNFTokenIDs(1);
                 nfTokens[0].setFieldVL(sfURI, Blob{});
 
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, nfTokens);
 
                 ac.view().insert(nftPage);
@@ -1222,9 +1227,9 @@ class Invariants_test : public beast::unit_test::Suite
         doInvariantCheck(
             {{"NFT page is improperly linked"}},
             [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
-                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftpageMax(a1).key);
+                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMax(a1).key);
 
                 ac.view().insert(nftPage);
                 return true;
@@ -1233,9 +1238,9 @@ class Invariants_test : public beast::unit_test::Suite
         doInvariantCheck(
             {{"NFT page is improperly linked"}},
             [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
-                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftpageMin(a2).key);
+                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMin(a2).key);
 
                 ac.view().insert(nftPage);
                 return true;
@@ -1244,7 +1249,7 @@ class Invariants_test : public beast::unit_test::Suite
         doInvariantCheck(
             {{"NFT page is improperly linked"}},
             [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftpageMax(a1));
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
                 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
                 nftPage->setFieldH256(sfNextPageMin, nftPage->key());
 
@@ -1256,10 +1261,10 @@ class Invariants_test : public beast::unit_test::Suite
             {{"NFT page is improperly linked"}},
             [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
                 STArray nfTokens = makeNFTokenIDs(1);
-                auto nftPage = std::make_shared(keylet::nftpage(
-                    keylet::nftpageMax(a1), ++(nfTokens[0].getFieldH256(sfNFTokenID))));
+                auto nftPage = std::make_shared(keylet::nftokenPage(
+                    keylet::nftokenPageMax(a1), ++(nfTokens[0].getFieldH256(sfNFTokenID))));
                 nftPage->setFieldArray(sfNFTokens, nfTokens);
-                nftPage->setFieldH256(sfNextPageMin, keylet::nftpageMax(a2).key);
+                nftPage->setFieldH256(sfNextPageMin, keylet::nftokenPageMax(a2).key);
 
                 ac.view().insert(nftPage);
                 return true;
@@ -1269,8 +1274,8 @@ class Invariants_test : public beast::unit_test::Suite
             {{"NFT found in incorrect page"}},
             [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
                 STArray nfTokens = makeNFTokenIDs(2);
-                auto nftPage = std::make_shared(keylet::nftpage(
-                    keylet::nftpageMax(a1), (nfTokens[1].getFieldH256(sfNFTokenID))));
+                auto nftPage = std::make_shared(keylet::nftokenPage(
+                    keylet::nftokenPageMax(a1), (nfTokens[1].getFieldH256(sfNFTokenID))));
                 nftPage->setFieldArray(sfNFTokens, nfTokens);
 
                 ac.view().insert(nftPage);
@@ -2129,7 +2134,7 @@ class Invariants_test : public beast::unit_test::Suite
 
         auto const getBookRootKey = [](Account const& account, std::uint64_t quality) {
             Book const book{xrpIssue(), account["USD"], std::nullopt};
-            return keylet::quality(keylet::kBook(book), quality);
+            return keylet::quality(keylet::book(book), quality);
         };
 
         // Root book-directory pages carry exchange-rate metadata that must
@@ -2301,7 +2306,7 @@ class Invariants_test : public beast::unit_test::Suite
         // Create Loan Broker
         using namespace loanBroker;
 
-        auto const loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a));
+        auto const loanBrokerKeylet = keylet::loanBroker(a.id(), env.seq(a));
         // Create a Loan Broker with all default values.
         env(set(a, vaultID), Fee(kIncrement));
 
@@ -2680,7 +2685,7 @@ class Invariants_test : public beast::unit_test::Suite
                 return false;
 
             auto const mptIssuanceID = (*sleVault)[sfShareMPTID];
-            auto sleShares = ac.peek(keylet::mptIssuance(mptIssuanceID));
+            auto sleShares = ac.peek(keylet::mptokenIssuance(mptIssuanceID));
             if (!sleShares)
                 return false;
 
@@ -2982,7 +2987,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto sleVault = ac.view().peek(keylet);
                 if (!sleVault)
                     return false;
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 ac.view().erase(sleVault);
@@ -3007,7 +3012,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto sleVault = ac.view().peek(keylet);
                 if (!sleVault)
                     return false;
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 // Note, such an "orphaned" update of MPT issuance attached to a
@@ -3097,7 +3102,7 @@ class Invariants_test : public beast::unit_test::Suite
                 (*sleVault)[sfAssetsMaximum] = 200;
                 ac.view().update(sleVault);
 
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 ac.view().erase(sleShares);
@@ -3293,7 +3298,7 @@ class Invariants_test : public beast::unit_test::Suite
                 if (!sleVault)
                     return false;
                 ac.view().update(sleVault);
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 (*sleShares)[sfOutstandingAmount] = 0;
@@ -3313,7 +3318,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto sleVault = ac.view().peek(keylet);
                 if (!sleVault)
                     return false;
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 (*sleShares)[sfMaximumAmount] = 10;
@@ -3336,7 +3341,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto sleVault = ac.view().peek(keylet);
                 if (!sleVault)
                     return false;
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 (*sleShares)[sfOutstandingAmount] = kMaxMpTokenAmount + 1;
@@ -3438,7 +3443,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto sleVault = ac.view().peek(keylet);
                 if (!sleVault)
                     return false;
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 ac.view().update(sleVault);
@@ -3490,7 +3495,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto sleVault = ac.view().peek(keylet);
                 if (!sleVault)
                     return false;
-                auto sleShares = ac.view().peek(keylet::mptIssuance((*sleVault)[sfShareMPTID]));
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
                 if (!sleShares)
                     return false;
                 ac.view().update(sleVault);
@@ -3537,7 +3542,7 @@ class Invariants_test : public beast::unit_test::Suite
                 ac.view().insert(sleAccount);
 
                 auto const sharesMptId = makeMptID(sequence, pseudoId);
-                auto const sharesKeylet = keylet::mptIssuance(sharesMptId);
+                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
                 auto sleShares = std::make_shared(sharesKeylet);
                 auto const sharesPage = ac.view().dirInsert(
                     keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
@@ -3595,7 +3600,7 @@ class Invariants_test : public beast::unit_test::Suite
                 ac.view().insert(sleAccount);
 
                 auto const sharesMptId = makeMptID(sequence, pseudoId);
-                auto const sharesKeylet = keylet::mptIssuance(sharesMptId);
+                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
                 auto sleShares = std::make_shared(sharesKeylet);
                 auto const sharesPage = ac.view().dirInsert(
                     keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
@@ -3637,7 +3642,7 @@ class Invariants_test : public beast::unit_test::Suite
                 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
 
                 auto const sharesMptId = makeMptID(sequence, a2.id());
-                auto const sharesKeylet = keylet::mptIssuance(sharesMptId);
+                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
                 auto sleShares = std::make_shared(sharesKeylet);
                 auto const sharesPage = ac.view().dirInsert(
                     keylet::ownerDir(a2.id()), sharesKeylet, describeOwnerDir(a2.id()));
@@ -4277,7 +4282,7 @@ class Invariants_test : public beast::unit_test::Suite
                     return false;
 
                 MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 sleNew->setFieldU64(sfOutstandingAmount, 110);
                 sleNew->setFieldU64(sfMaximumAmount, 100);
                 ac.view().insert(sleNew);
@@ -4294,7 +4299,7 @@ class Invariants_test : public beast::unit_test::Suite
                     return false;
 
                 MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 sleNew->setFieldU64(sfOutstandingAmount, 100);
                 sleNew->setFieldU64(sfMaximumAmount, 100);
                 ac.view().insert(sleNew);
@@ -4338,7 +4343,7 @@ class Invariants_test : public beast::unit_test::Suite
             });
         testPayment(
             "OutstandingAmount overflow", [&](MPTID const& id, ApplyContext& ac, Account const&) {
-                auto sle = ac.view().peek(keylet::mptIssuance(id));
+                auto sle = ac.view().peek(keylet::mptokenIssuance(id));
                 if (!sle)
                     return false;
                 sle->setFieldU64(sfOutstandingAmount, 101);
@@ -4365,7 +4370,8 @@ class Invariants_test : public beast::unit_test::Suite
                     for (int i = 0; i < nTokens; ++i)
                     {
                         MPTIssue const mpt{makeMptID(seq + i, a1)};
-                        auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                        auto sleNew =
+                            std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                         ac.view().insert(sleNew);
 
                         sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a2));
@@ -4419,7 +4425,7 @@ class Invariants_test : public beast::unit_test::Suite
                 if (!sleAcct)
                     return false;
                 MPTIssue const mpt{makeMptID(sleAcct->getFieldU32(sfSequence), a1)};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 sleNew->setFieldH256(sfReferenceHolding, uint256{1});
                 ac.view().insert(sleNew);
                 return true;
@@ -4442,7 +4448,7 @@ class Invariants_test : public beast::unit_test::Suite
                     if (!sleVault)
                         return false;
                     auto sleIssuance =
-                        ac.view().peek(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
+                        ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
                     if (!sleIssuance)
                         return false;
                     sleIssuance->setFieldH256(sfReferenceHolding, uint256{2});
@@ -4484,7 +4490,7 @@ class Invariants_test : public beast::unit_test::Suite
                     if (!sleVault)
                         return false;
                     auto const sleIssuance =
-                        ac.view().peek(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
+                        ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
                     if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
                         return false;
                     auto sleHolding = ac.view().peek(
@@ -4550,7 +4556,7 @@ class Invariants_test : public beast::unit_test::Suite
                                 ac.view().update(sle);
                                 return true;
                             };
-                            auto issuanceSle = ac.view().peek(keylet::mptIssuance(id));
+                            auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
                             if (!issuanceSle)
                                 return false;
                             auto const flags = issuanceSle->at(sfFlags);
@@ -4724,8 +4730,8 @@ class Invariants_test : public beast::unit_test::Suite
                                                    auto const& goodConfig) {
             char const* const c1 = "USD";
             char const* const c2 = "EUR";
-            auto const k1 = keylet::line(a1, a2, a1[c1].currency);
-            auto const k2 = keylet::line(a1, a3, a1[c2].currency);
+            auto const k1 = keylet::trustLine(a1, a2, a1[c1].currency);
+            auto const k2 = keylet::trustLine(a1, a3, a1[c2].currency);
 
             bool const k1First = k1.key < k2.key;
             auto const& badKey = k1First ? k1 : k2;
@@ -4820,7 +4826,7 @@ class Invariants_test : public beast::unit_test::Suite
                     return false;
 
                 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptIssuance(mpt.getMptID()));
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
                 // outstanding exceeds kMaxMpTokenAmount -> checkAmount sets bad_
                 sleNew->setFieldU64(sfOutstandingAmount, kMaxMpTokenAmount + 1);
                 // locked is valid and <= outstanding -> must NOT clear bad_
diff --git a/src/test/app/LPTokenTransfer_test.cpp b/src/test/app/LPTokenTransfer_test.cpp
index 4bf2db9515..2947b3a3ce 100644
--- a/src/test/app/LPTokenTransfer_test.cpp
+++ b/src/test/app/LPTokenTransfer_test.cpp
@@ -359,7 +359,7 @@ class LPTokenTransfer_test : public jtx::AMMTest
         env.close();
 
         // bob_ creates a sell offer for lptoken
-        uint256 const sellOfferIndex = keylet::nftoffer(bob_, env.seq(bob_)).key;
+        uint256 const sellOfferIndex = keylet::nftokenOffer(bob_, env.seq(bob_)).key;
         env(token::createOffer(bob_, nftID, STAmount{lpIssue, 10}), Txflags(tfSellNFToken));
         env.close();
 
@@ -420,7 +420,7 @@ class LPTokenTransfer_test : public jtx::AMMTest
             env.close();
 
             // bob_ creates a buy offer with lptoken despite bob_'s USD is frozen
-            uint256 const buyOfferIndex = keylet::nftoffer(bob_, env.seq(bob_)).key;
+            uint256 const buyOfferIndex = keylet::nftokenOffer(bob_, env.seq(bob_)).key;
             env(token::createOffer(bob_, nftID, STAmount{lpIssue, 10}), token::Owner(carol_));
             env.close();
 
diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp
index 0edb955b90..671f98a901 100644
--- a/src/test/app/LoanBroker_test.cpp
+++ b/src/test/app/LoanBroker_test.cpp
@@ -94,7 +94,7 @@ class LoanBroker_test : public beast::unit_test::Suite
             using namespace loanBroker;
             // Can't create a loan broker regardless of whether the vault exists
             env(set(alice, keylet.key), Ter(temDISABLED));
-            auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+            auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
             // Other LoanBroker transactions are disabled, too.
             // 1. LoanBrokerCoverDeposit
             env(coverDeposit(alice, brokerKeylet.key, asset(1000)), Ter(temDISABLED));
@@ -180,7 +180,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         static PrettyAsset const kGhostIouAsset = kNonExistent["GST"];
         PrettyAsset const vaultPseudoIouAsset = vault.pseudoAccount["PSD"];
 
-        auto const badKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const badKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
         env(set(alice, badVault.vaultID));
         env.close();
         auto const badBrokerPseudo = [&]() {
@@ -193,7 +193,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         }();
         PrettyAsset const badBrokerPseudoIouAsset = badBrokerPseudo["WAT"];
 
-        auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const keylet = keylet::loanBroker(alice.id(), env.seq(alice));
         {
             // Start with default values
             auto jtx = env.jt(set(alice, vault.vaultID));
@@ -736,7 +736,7 @@ class LoanBroker_test : public beast::unit_test::Suite
                     // Modifications
 
                     // Update the fields
-                    auto const nextKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+                    auto const nextKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
 
                     // fields that can't be changed
                     // LoanBrokerID
@@ -892,7 +892,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
         env.close();
 
-        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
         env(set(alice, vaultInfo.vaultID));
         env.close();
 
@@ -1231,7 +1231,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         env.close();
 
         // Predict LoanBroker key using alice's current sequence BEFORE submit
-        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
 
         // Create LoanBroker pointing to the vault
         env(loanBroker::set(alice, vaultKeylet.key));
@@ -1250,7 +1250,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         beast::Journal const jlog{sink};
         ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
 
-        if (auto sleBroker = ac.view().peek(keylet::loanbroker(brokerKeylet.key)))
+        if (auto sleBroker = ac.view().peek(keylet::loanBroker(brokerKeylet.key)))
         {
             auto const vaultID = (*sleBroker)[sfVaultID];
             if (auto sleVault = ac.view().peek(keylet::vault(vaultID)))
@@ -1337,7 +1337,7 @@ class LoanBroker_test : public beast::unit_test::Suite
                 err);
         });
 
-        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
         // Can create LoanBroker if the vault owner is not authorized
         forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); });
 
@@ -1415,7 +1415,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
         env.close();
 
-        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
         env(set(alice, vaultInfo.vaultID));
         env.close();
 
@@ -1503,7 +1503,7 @@ class LoanBroker_test : public beast::unit_test::Suite
                 Ter(err));
             env.close();
 
-            auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));
+            auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker));
 
             env(loanBroker::set(broker, keylet.key));
             env.close();
@@ -1617,7 +1617,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         env.close();
 
         // Create loan broker
-        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
         env(set(alice, vaultKeylet.key));
         env.close();
 
@@ -1734,7 +1734,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         env.close();
 
         // Create loan broker
-        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
         env(set(alice, vaultKeylet.key));
         env.close();
 
@@ -1876,7 +1876,7 @@ class LoanBroker_test : public beast::unit_test::Suite
             env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
 
             // Test LoanBroker withdraw
-            auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));
+            auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker));
 
             env(loanBroker::set(broker, keylet.key));
             env.close();
@@ -2004,7 +2004,7 @@ class LoanBroker_test : public beast::unit_test::Suite
             }
 
             // Test LoanBroker withdraw
-            auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));
+            auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker));
 
             env(loanBroker::set(broker, keylet.key));
             env.close();
@@ -2068,7 +2068,7 @@ class LoanBroker_test : public beast::unit_test::Suite
             env(createTx);
             env.close();
 
-            auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+            auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
             env(set(alice, vaultKeylet.key));
             env.close();
 
@@ -2215,7 +2215,7 @@ class LoanBroker_test : public beast::unit_test::Suite
                 env(createTx);
                 env.close();
 
-                auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
+                auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
                 env(set(alice, vaultKeylet.key));
                 env.close();
 
diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp
index 2d70efd9b2..1b0dd414e1 100644
--- a/src/test/app/Loan_test.cpp
+++ b/src/test/app/Loan_test.cpp
@@ -127,7 +127,7 @@ protected:
             Account const bob{"bob"};
             env.fund(XRP(10000), alice, bob);
 
-            auto const keylet = keylet::loanbroker(alice, env.seq(alice));
+            auto const keylet = keylet::loanBroker(alice, env.seq(alice));
 
             using namespace std::chrono_literals;
             using namespace loan;
@@ -214,7 +214,7 @@ protected:
         [[nodiscard]] Keylet
         brokerKeylet() const
         {
-            return keylet::loanbroker(brokerID);
+            return keylet::loanBroker(brokerID);
         }
         [[nodiscard]] Keylet
         vaultKeylet() const
@@ -371,7 +371,7 @@ protected:
             std::uint32_t ownerCount) const
         {
             using namespace jtx;
-            if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
                 env.test.BEAST_EXPECT(brokerSle))
             {
                 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
@@ -469,7 +469,7 @@ protected:
                     paymentRemaining,
                     1);
 
-                if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+                if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
                     env.test.BEAST_EXPECT(brokerSle))
                 {
                     if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
@@ -538,7 +538,7 @@ protected:
             BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value());
         }
 
-        auto const keylet = keylet::loanbroker(lender.id(), env.seq(lender));
+        auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender));
 
         using namespace loanBroker;
         env(set(lender, vaultKeylet.key, params.flags),
@@ -631,7 +631,7 @@ protected:
     bool
     canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state)
     {
-        if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
@@ -802,7 +802,7 @@ protected:
         BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams);
 
         auto const pseudoAcctOpt = [&]() -> std::optional {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return std::nullopt;
             auto const brokerPseudo = brokerSle->at(sfAccount);
@@ -813,7 +813,7 @@ protected:
         Account const& pseudoAcct = *pseudoAcctOpt;
 
         auto const loanKeyletOpt = [&]() -> std::optional {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return std::nullopt;
 
@@ -1287,7 +1287,7 @@ protected:
             toEndOfLife)
     {
         auto const [keylet, loanSequence] = [&]() {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
             {
                 // will be invalid
@@ -1376,7 +1376,7 @@ protected:
 
         auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count();
 
-        if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1);
@@ -1557,7 +1557,7 @@ protected:
             borrowerStartingBalance.value() - adjustment);
         BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
 
-        if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
@@ -1633,7 +1633,7 @@ protected:
         auto const loanSetFee = Fee(env.current()->fees().base * 2);
 
         auto const pseudoAcct = [&]() {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return Account{lender};
             auto const brokerPseudo = brokerSle->at(sfAccount);
@@ -1896,7 +1896,7 @@ protected:
         // XRP can not be frozen, but run through the loop anyway to test
         // the tecLIMIT_EXCEEDED case
         {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return;
 
@@ -2020,7 +2020,7 @@ protected:
         // Finally! Create a loan
 
         auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) {
-            if (auto const brokerSle = env.le(keylet::loanbroker(brokerID));
+            if (auto const brokerSle = env.le(keylet::loanBroker(brokerID));
                 BEAST_EXPECT(brokerSle))
             {
                 auto const available = brokerSle->at(sfCoverAvailable);
@@ -2030,7 +2030,7 @@ protected:
             return Number{};
         };
         auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) {
-            if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
                 BEAST_EXPECT(brokerSle))
             {
                 BEAST_EXPECT(
@@ -3149,7 +3149,7 @@ protected:
 
                 env(pay(borrower, issuer, broker.asset(10'000)));
                 env.close();
-                auto const trustline = keylet::line(borrower, broker.asset.raw().get());
+                auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get());
                 auto const sleLine1 = env.le(trustline);
                 BEAST_EXPECT(sleLine1 == nullptr);
 
@@ -3242,7 +3242,7 @@ protected:
                 env.trust(broker.asset(0), lender);
                 env.close();
 
-                auto const trustline = keylet::line(lender, broker.asset.raw().get());
+                auto const trustline = keylet::trustLine(lender, broker.asset.raw().get());
                 auto const sleLine1 = env.le(trustline);
                 BEAST_EXPECT(sleLine1 != nullptr);
 
@@ -3552,7 +3552,7 @@ protected:
                 }
             }
 
-            if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
                 BEAST_EXPECT(brokerSle))
             {
                 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
@@ -3563,7 +3563,7 @@ protected:
                     lender, broker.brokerID, STAmount(broker.asset, coverAvailable)));
                 env.close();
 
-                brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+                brokerSle = env.le(keylet::loanBroker(broker.brokerID));
                 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
             }
             // Verify we can delete the loan broker
@@ -3795,7 +3795,7 @@ protected:
 
         BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
 
-        if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0);
@@ -3865,7 +3865,7 @@ protected:
         createJson["OverpaymentInterestRate"] = 1360;
         createJson["PaymentInterval"] = 727;
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -4208,11 +4208,11 @@ protected:
             Env env(*this);
 
             auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) {
-                if (auto const le = env.le(keylet::loanbroker(brokerInfo.brokerID));
+                if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID));
                     BEAST_EXPECT(le))
                 {
                     auto const account = le->at(accountField);
-                    if (auto const sleLine = env.le(keylet::line(account, iou));
+                    if (auto const sleLine = env.le(keylet::trustLine(account, iou));
                         BEAST_EXPECT(sleLine))
                     {
                         STAmount balance = sleLine->at(sfBalance);
@@ -4390,7 +4390,7 @@ protected:
         env.close();
 
         auto const pseudoBroker = [&]() -> std::optional {
-            if (auto brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+            if (auto brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
                 BEAST_EXPECT(brokerSle))
             {
                 return Account{"pseudo", brokerSle->at(sfAccount)};
@@ -4620,7 +4620,7 @@ protected:
         createJson["PaymentTotal"] = "2891743748";
         createJson["PrincipalRequested"] = "8516.98";
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
 
         createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
         env(createJson, Ter(temINVALID));
@@ -4681,7 +4681,7 @@ protected:
         createJson["PaymentTotal"] = 5678;
         createJson["PrincipalRequested"] = "9924.81";
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -4690,7 +4690,7 @@ protected:
         env.close();
 
         auto const pseudoAcct = [&]() {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return Account{lender};
             auto const brokerPseudo = brokerSle->at(sfAccount);
@@ -4771,7 +4771,7 @@ protected:
         createJson["PaymentTotal"] = 1;
         createJson["PrincipalRequested"] = "0.000763058";
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -4840,7 +4840,7 @@ protected:
         // There are enough payments due on this loan that it only needs to be
         // created once, and can be paid on multiple times. Just don't create a
         // gazillion test cases.
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -4979,7 +4979,7 @@ protected:
         createJson["PaymentTotal"] = 5678;
         createJson["PrincipalRequested"] = "9924.81";
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -5085,7 +5085,7 @@ protected:
         createJson["PaymentTotal"] = 5678;
         createJson["PrincipalRequested"] = "9924.81";
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -5250,7 +5250,7 @@ protected:
         }
         {
             // Start date when the ledger is closed will be larger
-            auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
             auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
             auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -5277,7 +5277,7 @@ protected:
         }
         {
             // Start date when the ledger is closed will be larger
-            auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
             auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
             auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -5325,7 +5325,7 @@ protected:
             if (!BEAST_EXPECT(total != 0))
                 return;
 
-            auto const brokerState = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerState = env.le(keylet::loanBroker(broker.brokerID));
             // Intentionally shadow the outer values
             auto const loanSequence = brokerState->at(sfLoanSequence);
             auto const keylet = keylet::loan(broker.brokerID, loanSequence);
@@ -5514,7 +5514,7 @@ protected:
 
         auto const loanSetFee = Fee(env.current()->fees().base * 2);
 
-        auto const brokerPreLoan = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID));
         if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
             return;
 
@@ -5549,7 +5549,7 @@ protected:
         auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}};
         env.close(overdueClose);
 
-        auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSle = env.le(loanKeylet);
         if (!BEAST_EXPECT(brokerSle && loanSle))
             return;
@@ -5684,7 +5684,7 @@ protected:
             env(createTx);
             env.close();
 
-            auto const brokerBefore = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerBefore = env.le(keylet::loanBroker(broker.brokerID));
             BEAST_EXPECT(brokerBefore);
             if (!brokerBefore)
                 return;
@@ -5701,7 +5701,7 @@ protected:
             env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID));
             env.close();
 
-            auto const brokerAfter = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerAfter = env.le(keylet::loanBroker(broker.brokerID));
             BEAST_EXPECT(brokerAfter);
             if (!brokerAfter)
                 return;
@@ -5793,7 +5793,7 @@ protected:
             kGracePeriod(grace),
             Fee(loanSetFee));
 
-        auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
         BEAST_EXPECT(brokerSle);
         auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
         auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
@@ -5825,7 +5825,7 @@ protected:
         auto after = getCurrentState(env, broker, loanKeylet);
         auto const loanSle = env.le(loanKeylet);
         BEAST_EXPECT(loanSle);
-        auto const brokerSle2 = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID));
         BEAST_EXPECT(brokerSle2);
 
         auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{};
@@ -5987,7 +5987,7 @@ protected:
         auto broker = createVaultAndBroker(env, asset, lender, brokerParams);
 
         auto const loanKeyletOpt = [&]() -> std::optional {
-            auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return std::nullopt;
 
@@ -6258,7 +6258,7 @@ protected:
             txFee);
         env.close();
 
-        auto const brokerKeyLet = keylet::loanbroker(lender.id(), env.seq(lender));
+        auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender));
 
         env(loanBroker::set(lender, vaultKeyLet.key), txFee);
         env.close();
@@ -6332,7 +6332,7 @@ protected:
         env.close();
 
         // Verify DebtTotal is exactly 804
-        if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             log << *brokerSle << std::endl;
@@ -6353,7 +6353,7 @@ protected:
         env.close();
 
         // Validate CoverAvailable == 80 XRP and DebtTotal remains 804
-        if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             log << *brokerSle << std::endl;
@@ -6463,7 +6463,7 @@ protected:
                 txFee);
             env.close();
 
-            auto const brokerKeylet = keylet::loanbroker(broker.id(), env.seq(broker));
+            auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker));
 
             env(loanBroker::set(broker, vaultKeylet.key), txFee);
             env.close();
@@ -6747,7 +6747,7 @@ protected:
         env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover}));
         env.close();
         // Verify broker owner has a trustline
-        auto const brokerTrustline = keylet::line(broker, iou);
+        auto const brokerTrustline = keylet::trustLine(broker, iou);
         BEAST_EXPECT(env.le(brokerTrustline) != nullptr);
         // Broker owner deletes their trustline
         // First, pay any positive balance to issuer to zero it out
@@ -6766,7 +6766,7 @@ protected:
         // Verify trustline is still deleted
         BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
         // Verify the service fee went to the broker pseudo-account
-        if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
@@ -6847,7 +6847,7 @@ protected:
         // Verify the MPT is still unauthorized.
         BEAST_EXPECT(env.le(brokerMpt) == nullptr);
         // Verify the service fee went to the broker pseudo-account
-        if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
@@ -6950,7 +6950,7 @@ protected:
         // Verify broker is still not authorized
         env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
         // Verify the service fee went to the broker pseudo-account
-        if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+        if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
             BEAST_EXPECT(brokerSle))
         {
             Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
@@ -7186,7 +7186,7 @@ protected:
                 .coverDeposit = 500'000,
             });
         auto const [currentSeq, vaultKeylet] = [&]() {
-            auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
+            auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
             if (!BEAST_EXPECT(brokerSle))
                 return std::make_tuple(0u, keylet::unchecked(beast::kZero));
             auto const currentSeq = brokerSle->at(sfLoanSequence);
@@ -7358,7 +7358,7 @@ protected:
         env.close();
 
         // Create a loan
-        auto const sleBroker = env.le(keylet::loanbroker(broker.brokerID));
+        auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
         if (!BEAST_EXPECT(sleBroker))
             return;
 
@@ -7899,7 +7899,7 @@ protected:
         env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)}));
         env.close();
 
-        auto const brokerKeylet = keylet::loanbroker(lender.id(), env.seq(lender));
+        auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender));
         env(loanBroker::set(lender, vaultKeylet.key),
             loanBroker::kDebtMaximum(Number{100}),
             Fee(env.current()->fees().base * 2));
@@ -8006,7 +8006,7 @@ protected:
         createJson["PaymentTotal"] = 3;
         createJson["PaymentInterval"] = 600;
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const keylet = keylet::loan(broker.brokerID, loanSequence);
 
@@ -8089,7 +8089,7 @@ protected:
         createJson["PaymentTotal"] = 3;
         createJson["PaymentInterval"] = 600;
 
-        auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
+        auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
         auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
         auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
         createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
@@ -8231,7 +8231,7 @@ protected:
                 // Create the TINY loan first (while vaultScale is still
                 // small).  principal 0.01, 0% interest, 1 payment =>
                 // loanScale = vaultScale.
-                auto const brokerSle1 = env.le(keylet::loanbroker(c.broker.brokerID));
+                auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID));
                 if (!BEAST_EXPECT(brokerSle1))
                     return std::nullopt;
                 auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence);
@@ -8248,7 +8248,7 @@ protected:
                 // Create the BIG loan second.  100% annual interest over 20
                 // payments pushes totalValueOutstanding high enough that
                 // loanScale > vaultScale.
-                auto const brokerSle2 = env.le(keylet::loanbroker(c.broker.brokerID));
+                auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID));
                 if (!BEAST_EXPECT(brokerSle2))
                     return std::nullopt;
                 auto const bigLoanSeq = brokerSle2->at(sfLoanSequence);
@@ -8299,7 +8299,7 @@ protected:
                     kAmount(STAmount{asset, clawbackAmount}));
                 env.close();
 
-                auto const brokerSle = env.le(keylet::loanbroker(c.broker.brokerID));
+                auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
                 if (!BEAST_EXPECT(brokerSle) ||
                     !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter))
                     return std::nullopt;
@@ -8312,7 +8312,7 @@ protected:
             // than to the owner.
             auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool {
                 Asset const asset{c.iou};
-                auto const brokerSle = env.le(keylet::loanbroker(c.broker.brokerID));
+                auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
                 if (!BEAST_EXPECT(brokerSle))
                     return false;
                 auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount));
@@ -8394,7 +8394,7 @@ protected:
                 env.close();
 
                 // Read broker state and compute both old and new minimums.
-                auto const brokerSle = env.le(keylet::loanbroker(c.broker.brokerID));
+                auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
                 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
                 if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle))
                     return;
diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp
index 2cab3e7c89..bfb80d3e36 100644
--- a/src/test/app/MPToken_test.cpp
+++ b/src/test/app/MPToken_test.cpp
@@ -4049,7 +4049,7 @@ class MPToken_test : public beast::unit_test::Suite
             // view may contain partial state and must be discarded.
             if (expectedOutstanding)
             {
-                auto const sle = av.peek(keylet::mptIssuance(mptTester.issuanceID()));
+                auto const sle = av.peek(keylet::mptokenIssuance(mptTester.issuanceID()));
                 if (!BEAST_EXPECT(sle))
                     return;
                 BEAST_EXPECTS(sle->getFieldU64(sfOutstandingAmount) == *expectedOutstanding, label);
diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp
index f161226210..8fb1eb31ab 100644
--- a/src/test/app/MultiSign_test.cpp
+++ b/src/test/app/MultiSign_test.cpp
@@ -1511,7 +1511,7 @@ public:
         env.close();
 
         // Verify that the SignerList object was created correctly.
-        auto const& sle = env.le(keylet::signers(alice.id()));
+        auto const& sle = env.le(keylet::signerList(alice.id()));
         BEAST_EXPECT(sle);
         BEAST_EXPECT(sle->getFieldArray(sfSignerEntries).size() == 2);
         if (features[fixIncludeKeyletFields])
diff --git a/src/test/app/NFTokenAuth_test.cpp b/src/test/app/NFTokenAuth_test.cpp
index 4929cadd65..66716a13b7 100644
--- a/src/test/app/NFTokenAuth_test.cpp
+++ b/src/test/app/NFTokenAuth_test.cpp
@@ -43,7 +43,7 @@ class NFTokenAuth_test : public beast::unit_test::Suite
         env(token::mint(account, 0), token::XferFee(xfee), Txflags(tfTransferable));
         env.close();
 
-        auto const sellIdx = keylet::nftoffer(account, env.seq(account)).key;
+        auto const sellIdx = keylet::nftokenOffer(account, env.seq(account)).key;
         env(token::createOffer(account, nftID, currency), Txflags(tfSellNFToken));
         env.close();
 
@@ -74,7 +74,7 @@ public:
         env(pay(g1, a1, usd(1000)));
 
         auto const [nftID, _] = mintAndOfferNFT(env, a2, drops(1));
-        auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+        auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
 
         // It should be possible to create a buy offer even if NFT owner is not
         // authorized
@@ -130,7 +130,7 @@ public:
         // close ledger before running the actual tests against this trustline.
         // After ledger is closed, the trustline will not exist.
         auto const unauthTrustline = [&](OpenView& view, beast::Journal) -> bool {
-            auto const sleA1 = std::make_shared(keylet::line(a1, g1, g1["USD"].currency));
+            auto const sleA1 = std::make_shared(keylet::trustLine(a1, g1, g1["USD"].currency));
             sleA1->setFieldAmount(sfBalance, a1["USD"](-1000));
             view.rawInsert(sleA1);
             return true;
@@ -179,7 +179,7 @@ public:
         env(pay(g1, a2, usd(10)));
         env.close();
 
-        auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+        auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
         env(token::createOffer(a1, nftID, usd(10)), token::Owner(a2));
         env.close();
 
@@ -193,7 +193,7 @@ public:
         // tests against this trustline. After ledger is closed, the trustline
         // will not exist.
         auto const unauthTrustline = [&](OpenView& view, beast::Journal) -> bool {
-            auto const sleA1 = std::make_shared(keylet::line(a1, g1, g1["USD"].currency));
+            auto const sleA1 = std::make_shared(keylet::trustLine(a1, g1, g1["USD"].currency));
             sleA1->setFieldAmount(sfBalance, a1["USD"](-1000));
             view.rawInsert(sleA1);
             return true;
@@ -244,7 +244,7 @@ public:
             // Authorizing trustline to make an offer creation possible
             env(trust(g1, usd(0), a2, tfSetfAuth));
             env.close();
-            auto const sellIdx = keylet::nftoffer(a2, env.seq(a2)).key;
+            auto const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key;
             env(token::createOffer(a2, nftID, usd(10)), Txflags(tfSellNFToken));
             env.close();
             //
@@ -268,7 +268,7 @@ public:
         }
         else
         {
-            auto const sellIdx = keylet::nftoffer(a2, env.seq(a2)).key;
+            auto const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key;
 
             // Old behavior: sell offer can be created without authorization
             env(token::createOffer(a2, nftID, usd(10)), Txflags(tfSellNFToken));
@@ -313,7 +313,7 @@ public:
 
         // Creating an artificial unauth trustline
         auto const unauthTrustline = [&](OpenView& view, beast::Journal) -> bool {
-            auto const sleA1 = std::make_shared(keylet::line(a1, g1, g1["USD"].currency));
+            auto const sleA1 = std::make_shared(keylet::trustLine(a1, g1, g1["USD"].currency));
             sleA1->setFieldAmount(sfBalance, a1["USD"](-1000));
             view.rawInsert(sleA1);
             return true;
@@ -353,7 +353,7 @@ public:
         env.close();
 
         auto const [nftID, sellIdx] = mintAndOfferNFT(env, a2, usd(10));
-        auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+        auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
         env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2));
         env.close();
 
@@ -422,7 +422,7 @@ public:
         env.close();
 
         auto const [nftID, sellIdx] = mintAndOfferNFT(env, a2, usd(10));
-        auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+        auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
         env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2));
         env.close();
 
@@ -432,7 +432,7 @@ public:
         env.close();
 
         auto const unauthTrustline = [&](OpenView& view, beast::Journal) -> bool {
-            auto const sleA1 = std::make_shared(keylet::line(a1, g1, g1["USD"].currency));
+            auto const sleA1 = std::make_shared(keylet::trustLine(a1, g1, g1["USD"].currency));
             sleA1->setFieldAmount(sfBalance, a1["USD"](-1000));
             view.rawInsert(sleA1);
             return true;
@@ -483,7 +483,7 @@ public:
         env.close();
 
         auto const [nftID, sellIdx] = mintAndOfferNFT(env, a2, usd(10));
-        auto const buyIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+        auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
         env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2));
         env.close();
 
@@ -559,7 +559,7 @@ public:
         auto const [nftID, minterSellIdx] = mintAndOfferNFT(env, minter, drops(1), 1);
         env(token::acceptSellOffer(a1, minterSellIdx));
 
-        uint256 const sellIdx = keylet::nftoffer(a1, env.seq(a1)).key;
+        uint256 const sellIdx = keylet::nftokenOffer(a1, env.seq(a1)).key;
         env(token::createOffer(a1, nftID, usd(100)), Txflags(tfSellNFToken));
 
         if (features[fixEnforceNFTokenTrustlineV2])
diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp
index 46b02d03cf..00667dc5ac 100644
--- a/src/test/app/NFTokenBurn_test.cpp
+++ b/src/test/app/NFTokenBurn_test.cpp
@@ -76,7 +76,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
         for (uint32_t i = 0; i < tokenCancelCount; ++i)
         {
             // Create sell offer
-            offerIndexes.push_back(keylet::nftoffer(owner, env.seq(owner)).key);
+            offerIndexes.push_back(keylet::nftokenOffer(owner, env.seq(owner)).key);
             env(token::createOffer(owner, nftokenID, drops(1)), Txflags(tfSellNFToken));
             env.close();
         }
@@ -235,7 +235,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             {
                 // We do the same work on alice and minter, so make a lambda.
                 auto xferNFT = [&env, &becky](AcctStat& acct, auto& iter) {
-                    uint256 const offerIndex = keylet::nftoffer(acct.acct, env.seq(acct.acct)).key;
+                    uint256 const offerIndex =
+                        keylet::nftokenOffer(acct.acct, env.seq(acct.acct)).key;
                     env(token::createOffer(acct, *iter, XRP(0)), Txflags(tfSellNFToken));
                     env.close();
                     env(token::acceptSellOffer(becky, offerIndex));
@@ -472,19 +473,19 @@ class NFTokenBurn_test : public beast::unit_test::Suite
 
             // Verify that that all three pages are present and remember the
             // indexes.
-            auto lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            auto lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             if (!BEAST_EXPECT(lastNFTokenPage))
                 return;
 
             uint256 const middleNFTokenPageIndex = lastNFTokenPage->at(sfPreviousPageMin);
             auto middleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
             if (!BEAST_EXPECT(middleNFTokenPage))
                 return;
 
             uint256 const firstNFTokenPageIndex = middleNFTokenPage->at(sfPreviousPageMin);
             auto firstNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
             if (!BEAST_EXPECT(firstNFTokenPage))
                 return;
 
@@ -498,7 +499,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
 
             // Verify that the last page is still present and contains just one
             // NFT.
-            lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             if (!BEAST_EXPECT(lastNFTokenPage))
                 return;
 
@@ -517,21 +518,21 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 // _previous_ page because we need to preserve that last
                 // page as an anchor.  The contents of the next-to-last page
                 // are moved into the last page.
-                lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+                lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
                 BEAST_EXPECT(lastNFTokenPage);
                 BEAST_EXPECT(lastNFTokenPage->at(~sfPreviousPageMin) == firstNFTokenPageIndex);
                 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
                 BEAST_EXPECT(lastNFTokenPage->getFieldArray(sfNFTokens).size() == 32);
 
                 // The "middle" page should be gone.
-                middleNFTokenPage =
-                    env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                middleNFTokenPage = env.le(
+                    keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
                 BEAST_EXPECT(!middleNFTokenPage);
 
                 // The "first" page should still be present and linked to
                 // the last page.
-                firstNFTokenPage =
-                    env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                firstNFTokenPage = env.le(
+                    keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
                 BEAST_EXPECT(firstNFTokenPage);
                 BEAST_EXPECT(!firstNFTokenPage->isFieldPresent(sfPreviousPageMin));
                 BEAST_EXPECT(firstNFTokenPage->at(~sfNextPageMin) == lastNFTokenPage->key());
@@ -542,13 +543,13 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 // Removing the last token from the last page deletes the last
                 // page.  This is a bug.  The contents of the next-to-last page
                 // should have been moved into the last page.
-                lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+                lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
                 BEAST_EXPECT(!lastNFTokenPage);
 
                 // The "middle" page is still present, but has lost the
                 // NextPageMin field.
-                middleNFTokenPage =
-                    env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                middleNFTokenPage = env.le(
+                    keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
                 if (!BEAST_EXPECT(middleNFTokenPage))
                     return;
                 BEAST_EXPECT(middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
@@ -576,19 +577,19 @@ class NFTokenBurn_test : public beast::unit_test::Suite
 
             // Verify that that all three pages are present and remember the
             // indexes.
-            auto lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            auto lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             if (!BEAST_EXPECT(lastNFTokenPage))
                 return;
 
             uint256 const middleNFTokenPageIndex = lastNFTokenPage->at(sfPreviousPageMin);
             auto middleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
             if (!BEAST_EXPECT(middleNFTokenPage))
                 return;
 
             uint256 const firstNFTokenPageIndex = middleNFTokenPage->at(sfPreviousPageMin);
             auto firstNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
             if (!BEAST_EXPECT(firstNFTokenPage))
                 return;
 
@@ -604,17 +605,17 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // Verify that middle page is gone and the links in the two
             // remaining pages are correct.
             middleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
             BEAST_EXPECT(!middleNFTokenPage);
 
-            lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
             BEAST_EXPECT(lastNFTokenPage->getFieldH256(sfPreviousPageMin) == firstNFTokenPageIndex);
 
             firstNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
             BEAST_EXPECT(
-                firstNFTokenPage->getFieldH256(sfNextPageMin) == keylet::nftpageMax(alice).key);
+                firstNFTokenPage->getFieldH256(sfNextPageMin) == keylet::nftokenPageMax(alice).key);
             BEAST_EXPECT(!firstNFTokenPage->isFieldPresent(sfPreviousPageMin));
 
             // Burn the remaining nfts.
@@ -637,19 +638,19 @@ class NFTokenBurn_test : public beast::unit_test::Suite
 
             // Verify that that all three pages are present and remember the
             // indexes.
-            auto lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            auto lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             if (!BEAST_EXPECT(lastNFTokenPage))
                 return;
 
             uint256 const middleNFTokenPageIndex = lastNFTokenPage->at(sfPreviousPageMin);
             auto middleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
             if (!BEAST_EXPECT(middleNFTokenPage))
                 return;
 
             uint256 const firstNFTokenPageIndex = middleNFTokenPage->at(sfPreviousPageMin);
             auto firstNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
             if (!BEAST_EXPECT(firstNFTokenPage))
                 return;
 
@@ -664,18 +665,18 @@ class NFTokenBurn_test : public beast::unit_test::Suite
 
             // Verify the first page is gone.
             firstNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
             BEAST_EXPECT(!firstNFTokenPage);
 
             // Check the links in the other two pages.
             middleNFTokenPage =
-                env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
             if (!BEAST_EXPECT(middleNFTokenPage))
                 return;
             BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
             BEAST_EXPECT(middleNFTokenPage->isFieldPresent(sfNextPageMin));
 
-            lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+            lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
             if (!BEAST_EXPECT(lastNFTokenPage))
                 return;
             BEAST_EXPECT(lastNFTokenPage->isFieldPresent(sfPreviousPageMin));
@@ -696,20 +697,20 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 // _previous_ page because we need to preserve that last
                 // page as an anchor.  The contents of the next-to-last page
                 // are moved into the last page.
-                lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+                lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
                 BEAST_EXPECT(lastNFTokenPage);
                 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfPreviousPageMin));
                 BEAST_EXPECT(!lastNFTokenPage->isFieldPresent(sfNextPageMin));
                 BEAST_EXPECT(lastNFTokenPage->getFieldArray(sfNFTokens).size() == 32);
 
                 // The "middle" page should be gone.
-                middleNFTokenPage =
-                    env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                middleNFTokenPage = env.le(
+                    keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
                 BEAST_EXPECT(!middleNFTokenPage);
 
                 // The "first" page should still be gone.
-                firstNFTokenPage =
-                    env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+                firstNFTokenPage = env.le(
+                    keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
                 BEAST_EXPECT(!firstNFTokenPage);
             }
             else
@@ -717,13 +718,13 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 // Removing the last token from the last page deletes the last
                 // page.  This is a bug.  The contents of the next-to-last page
                 // should have been moved into the last page.
-                lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+                lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
                 BEAST_EXPECT(!lastNFTokenPage);
 
                 // The "middle" page is still present, but has lost the
                 // NextPageMin field.
-                middleNFTokenPage =
-                    env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+                middleNFTokenPage = env.le(
+                    keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
                 if (!BEAST_EXPECT(middleNFTokenPage))
                     return;
                 BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
@@ -777,7 +778,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                     env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
 
                 // Verify that the last page is present and contains one NFT.
-                auto lastNFTokenPage = ac.view().peek(keylet::nftpageMax(alice));
+                auto lastNFTokenPage = ac.view().peek(keylet::nftokenPageMax(alice));
                 if (!BEAST_EXPECT(lastNFTokenPage))
                     return;
                 BEAST_EXPECT(lastNFTokenPage->getFieldArray(sfNFTokens).size() == 1);
@@ -809,10 +810,10 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                     env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
 
                 // Verify that the middle  page is present.
-                auto lastNFTokenPage = ac.view().peek(keylet::nftpageMax(alice));
+                auto lastNFTokenPage = ac.view().peek(keylet::nftokenPageMax(alice));
                 auto middleNFTokenPage = ac.view().peek(
-                    keylet::nftpage(
-                        keylet::nftpageMin(alice),
+                    keylet::nftokenPage(
+                        keylet::nftokenPageMin(alice),
                         lastNFTokenPage->getFieldH256(sfPreviousPageMin)));
                 BEAST_EXPECT(middleNFTokenPage);
 
@@ -865,11 +866,11 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // Verify all sell offers are present in the ledger.
             for (uint256 const& offerIndex : offerIndexes)
             {
-                BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
+                BEAST_EXPECT(env.le(keylet::nftokenOffer(offerIndex)));
             }
 
             // Becky creates a buy offer
-            uint256 const beckyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftokenID, drops(1)), token::Owner(alice));
             env.close();
 
@@ -881,12 +882,12 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // that alice created
             for (uint256 const& offerIndex : offerIndexes)
             {
-                BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
+                BEAST_EXPECT(!env.le(keylet::nftokenOffer(offerIndex)));
             }
 
             // Burning the token should also remove the one buy offer
             // that becky created
-            BEAST_EXPECT(!env.le(keylet::nftoffer(beckyOfferIndex)));
+            BEAST_EXPECT(!env.le(keylet::nftokenOffer(beckyOfferIndex)));
 
             // alice and becky should have ownerCounts of zero
             BEAST_EXPECT(ownerCount(env, alice) == 0);
@@ -912,7 +913,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // Verify all sell offers are present in the ledger.
             for (uint256 const& offerIndex : offerIndexes)
             {
-                BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
+                BEAST_EXPECT(env.le(keylet::nftokenOffer(offerIndex)));
             }
 
             // Burn the token
@@ -923,7 +924,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // Count the number of sell offers that have been deleted
             for (uint256 const& offerIndex : offerIndexes)
             {
-                if (!env.le(keylet::nftoffer(offerIndex)))
+                if (!env.le(keylet::nftokenOffer(offerIndex)))
                     offerDeletedCount++;
             }
 
@@ -956,7 +957,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // Verify all sell offers are present in the ledger.
             for (uint256 const& offerIndex : offerIndexes)
             {
-                BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
+                BEAST_EXPECT(env.le(keylet::nftokenOffer(offerIndex)));
             }
 
             // becky creates 2 buy offers
@@ -973,7 +974,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // ledger.
             for (uint256 const& offerIndex : offerIndexes)
             {
-                BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
+                BEAST_EXPECT(!env.le(keylet::nftokenOffer(offerIndex)));
             }
 
             // alice should have ownerCount of zero because all her
@@ -1044,7 +1045,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 env.close();
 
                 // Minter creates an offer for the NFToken.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nfts.back(), XRP(0)), Txflags(tfSellNFToken));
                 env.close();
 
@@ -1091,19 +1092,19 @@ class NFTokenBurn_test : public beast::unit_test::Suite
 
         // Verify that that all three pages are present and remember the
         // indexes.
-        auto lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+        auto lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
         if (!BEAST_EXPECT(lastNFTokenPage))
             return;
 
         uint256 const middleNFTokenPageIndex = lastNFTokenPage->at(sfPreviousPageMin);
         auto middleNFTokenPage =
-            env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+            env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
         if (!BEAST_EXPECT(middleNFTokenPage))
             return;
 
         uint256 const firstNFTokenPageIndex = middleNFTokenPage->at(sfPreviousPageMin);
         auto firstNFTokenPage =
-            env.le(keylet::nftpage(keylet::nftpageMin(alice), firstNFTokenPageIndex));
+            env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), firstNFTokenPageIndex));
         if (!BEAST_EXPECT(firstNFTokenPage))
             return;
 
@@ -1115,7 +1116,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             nfts.pop_back();
 
             // alice creates an offer for the NFToken.
-            uint256 const aliceOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, last32NFTs.back(), XRP(0)), Txflags(tfSellNFToken));
             env.close();
 
@@ -1127,14 +1128,14 @@ class NFTokenBurn_test : public beast::unit_test::Suite
         // Removing the last token from the last page deletes alice's last
         // page.  This is a bug.  The contents of the next-to-last page
         // should have been moved into the last page.
-        lastNFTokenPage = env.le(keylet::nftpageMax(alice));
+        lastNFTokenPage = env.le(keylet::nftokenPageMax(alice));
         BEAST_EXPECT(!lastNFTokenPage);
         BEAST_EXPECT(ownerCount(env, alice) == 2);
 
         // The "middle" page is still present, but has lost the
         // NextPageMin field.
         middleNFTokenPage =
-            env.le(keylet::nftpage(keylet::nftpageMin(alice), middleNFTokenPageIndex));
+            env.le(keylet::nftokenPage(keylet::nftokenPageMin(alice), middleNFTokenPageIndex));
         if (!BEAST_EXPECT(middleNFTokenPage))
             return;
         BEAST_EXPECT(middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
@@ -1149,7 +1150,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
         for (uint256 const nftID : last32NFTs)
         {
             // minter creates an offer for the NFToken.
-            uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken));
             env.close();
 
diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp
index e24a524b81..19bf58f247 100644
--- a/src/test/app/NFTokenDir_test.cpp
+++ b/src/test/app/NFTokenDir_test.cpp
@@ -142,7 +142,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
         std::vector offers;
         for (uint256 const& nftID : nftIDs)
         {
-            offers.emplace_back(keylet::nftoffer(issuer, env.seq(issuer)).key);
+            offers.emplace_back(keylet::nftokenOffer(issuer, env.seq(issuer)).key);
             env(token::createOffer(issuer, nftID, XRP(0)), Txflags(tfSellNFToken));
             env.close();
         }
@@ -214,7 +214,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
                 env.close();
 
                 // Create an offer to give the NFT to buyer for free.
-                offers.emplace_back(keylet::nftoffer(account, env.seq(account)).key);
+                offers.emplace_back(keylet::nftokenOffer(account, env.seq(account)).key);
                 env(token::createOffer(account, nftID, XRP(0)),
                     token::Destination(buyer),
                     Txflags(tfSellNFToken));
@@ -237,7 +237,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
             // generates a non-tesSUCCESS error code.
             for (uint256 const& nftID : nftIDs)
             {
-                uint256 const offerID = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const offerID = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
                 env.close();
 
@@ -418,7 +418,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
                 env.close();
 
                 // Create an offer to give the NFT to buyer for free.
-                offers.emplace_back(keylet::nftoffer(account, env.seq(account)).key);
+                offers.emplace_back(keylet::nftokenOffer(account, env.seq(account)).key);
                 env(token::createOffer(account, nftID, XRP(0)),
                     token::Destination(buyer),
                     Txflags(tfSellNFToken));
@@ -445,7 +445,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
             // generates a non-tesSUCCESS error code.
             for (uint256 const& nftID : nftIDs)
             {
-                uint256 const offerID = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const offerID = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
                 env.close();
 
@@ -648,7 +648,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
             env.close();
 
             // Create an offer to give the NFT to buyer for free.
-            offers.emplace_back(keylet::nftoffer(account, env.seq(account)).key);
+            offers.emplace_back(keylet::nftokenOffer(account, env.seq(account)).key);
             env(token::createOffer(account, nftID, XRP(0)),
                 token::Destination(buyer),
                 Txflags(tfSellNFToken));
@@ -684,7 +684,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
         // a non-tesSUCCESS error code.
         for (uint256 const& nftID : nftIDs)
         {
-            uint256 const offerID = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerID = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken));
             env.close();
 
@@ -820,7 +820,7 @@ class NFTokenDir_test : public beast::unit_test::Suite
                 env.close();
 
                 // Create an offer to give the NFT to buyer for free.
-                offers[i].emplace_back(keylet::nftoffer(account, env.seq(account)).key);
+                offers[i].emplace_back(keylet::nftokenOffer(account, env.seq(account)).key);
                 env(token::createOffer(account, nftID, XRP(0)),
                     token::Destination(buyer),
                     Txflags(tfSellNFToken));
diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp
index cb92b23a4c..ef7c385acb 100644
--- a/src/test/app/NFToken_test.cpp
+++ b/src/test/app/NFToken_test.cpp
@@ -143,7 +143,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             Account const alice{"alice"};
             env.fund(XRP(10000), alice);
             env.close();
-            uint256 const aliceOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId1, XRP(1000)), token::Owner(master));
             env.close();
 
@@ -861,7 +861,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(ownerCount(env, alice) == 1);
 
         // This is the offer we'll try to cancel.
-        uint256 const buyerOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+        uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
         env(token::createOffer(buyer, nftAlice0ID, XRP(1)), token::Owner(alice), Ter(tesSUCCESS));
         env.close();
         BEAST_EXPECT(ownerCount(env, buyer) == 1);
@@ -904,7 +904,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // List of offer IDs containing zero is invalid.
             // craftedIndex is not a valid offer index but it is not zero.
-            auto const craftedIndex = keylet::nftoffer(gw, env.seq(gw)).key;
+            auto const craftedIndex = keylet::nftokenOffer(gw, env.seq(gw)).key;
             env(token::cancelOffer(buyer, {buyerOfferIndex, uint256{}, craftedIndex}),
                 Ter(temMALFORMED));
             env.close();
@@ -1006,32 +1006,32 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
 
         // alice creates sell offers for her nfts.
-        uint256 const plainOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const plainOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
         env(token::createOffer(alice, nftAlice0ID, XRP(10)), Txflags(tfSellNFToken));
         env.close();
         aliceCount++;
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
 
-        uint256 const audOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const audOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
         env(token::createOffer(alice, nftAlice0ID, gwAUD(30)), Txflags(tfSellNFToken));
         env.close();
         aliceCount++;
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
 
-        uint256 const xrpOnlyOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const xrpOnlyOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
         env(token::createOffer(alice, nftXrpOnlyID, XRP(20)), Txflags(tfSellNFToken));
         env.close();
         aliceCount++;
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
 
-        uint256 const noXferOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const noXferOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
         env(token::createOffer(alice, nftNoXferID, XRP(30)), Txflags(tfSellNFToken));
         env.close();
         aliceCount++;
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
 
         // alice creates a sell offer that will expire soon.
-        uint256 const aliceExpOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const aliceExpOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
         env(token::createOffer(alice, nftNoXferID, XRP(40)),
             Txflags(tfSellNFToken),
             token::Expiration(lastClose(env) + 5));
@@ -1040,7 +1040,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
 
         // buyer creates a Buy offer that will expire soon.
-        uint256 const buyerExpOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+        uint256 const buyerExpOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
         env(token::createOffer(buyer, nftAlice0ID, XRP(40)),
             token::Owner(alice),
             token::Expiration(lastClose(env) + 5));
@@ -1108,7 +1108,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
 
         // The buy offer must be present in the ledger.
-        uint256 const missingOfferIndex = keylet::nftoffer(alice, 1).key;
+        uint256 const missingOfferIndex = keylet::nftokenOffer(alice, 1).key;
         env(token::acceptBuyOffer(buyer, missingOfferIndex), Ter(tecOBJECT_NOT_FOUND));
         env.close();
         BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
@@ -1120,7 +1120,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         if (features[fixCleanup3_1_3])
         {
             buyerCount--;
-            BEAST_EXPECT(!env.closed()->exists(keylet::nftoffer(buyerExpOfferIndex)));
+            BEAST_EXPECT(!env.closed()->exists(keylet::nftokenOffer(buyerExpOfferIndex)));
         }
         BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
 
@@ -1144,7 +1144,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         if (features[fixCleanup3_1_3])
         {
             aliceCount--;
-            BEAST_EXPECT(!env.closed()->exists(keylet::nftoffer(aliceExpOfferIndex)));
+            BEAST_EXPECT(!env.closed()->exists(keylet::nftokenOffer(aliceExpOfferIndex)));
         }
         BEAST_EXPECT(ownerCount(env, alice) == aliceCount);
         BEAST_EXPECT(ownerCount(env, buyer) == buyerCount);
@@ -1171,7 +1171,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // corresponding buy and sell offers.
         {
             // buyer creates a buy offer for one of alice's nfts.
-            uint256 const buyerOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftAlice0ID, gwAUD(29)), token::Owner(alice));
             env.close();
             buyerCount++;
@@ -1204,7 +1204,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         }
         {
             // buyer creates a buy offer for one of alice's nfts.
-            uint256 const buyerOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftAlice0ID, gwAUD(31)), token::Owner(alice));
             env.close();
             buyerCount++;
@@ -1243,7 +1243,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // preclaim buy
         {
             // buyer creates a buy offer for one of alice's nfts.
-            uint256 const buyerOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftAlice0ID, gwAUD(30)), token::Owner(alice));
             env.close();
             buyerCount++;
@@ -1270,7 +1270,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // alice gives her NFT to gw, so alice no longer owns nftAlice0.
             {
-                uint256 const offerIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+                uint256 const offerIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
                 env(token::createOffer(alice, nftAlice0ID, XRP(0)), Txflags(tfSellNFToken));
                 env.close();
                 env(token::acceptSellOffer(gw, offerIndex));
@@ -1295,7 +1295,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // preclaim sell
         {
             // buyer creates a buy offer for one of alice's nfts.
-            uint256 const buyerOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftXrpOnlyID, XRP(30)), token::Owner(alice));
             env.close();
             buyerCount++;
@@ -1323,7 +1323,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // buyer attempting to accept one of alice's offers with
             // insufficient funds.
             {
-                uint256 const offerIndex = keylet::nftoffer(gw, env.seq(gw)).key;
+                uint256 const offerIndex = keylet::nftokenOffer(gw, env.seq(gw)).key;
                 env(token::createOffer(gw, nftAlice0ID, XRP(0)), Txflags(tfSellNFToken));
                 env.close();
                 env(token::acceptSellOffer(alice, offerIndex));
@@ -1376,7 +1376,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env(token::mint(minter1, 0u), token::Issuer(alice), Txflags(flags));
             env.close();
 
-            uint256 const offerIndex = keylet::nftoffer(minter1, env.seq(minter1)).key;
+            uint256 const offerIndex = keylet::nftokenOffer(minter1, env.seq(minter1)).key;
             env(token::createOffer(minter1, nftID, XRP(0)), Txflags(tfSellNFToken));
             env.close();
 
@@ -1479,13 +1479,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             BEAST_EXPECT(ownerCount(env, alice) == 2);
-            uint256 const aliceOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftIOUsOkayID, gwAUD(50)), Txflags(tfSellNFToken));
             env.close();
             BEAST_EXPECT(ownerCount(env, alice) == 3);
 
             BEAST_EXPECT(ownerCount(env, buyer) == 1);
-            uint256 const buyerOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftIOUsOkayID, gwAUD(50)), token::Owner(alice));
             env.close();
             BEAST_EXPECT(ownerCount(env, buyer) == 2);
@@ -1588,7 +1588,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 env.close();
 
                 // becky buys the nft for 1 drop.
-                uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), token::Owner(alice));
                 env.close();
                 env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
@@ -1596,14 +1596,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
                 // becky attempts to sell the nft for AUD.
                 TER const createOfferTER = (xferFee != 0u) ? TER(tecNO_LINE) : TER(tesSUCCESS);
-                uint256 const beckyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)),
                     Txflags(tfSellNFToken),
                     Ter(createOfferTER));
                 env.close();
 
                 // cheri offers to buy the nft for CAD.
-                uint256 const cheriOfferIndex = keylet::nftoffer(cheri, env.seq(cheri)).key;
+                uint256 const cheriOfferIndex = keylet::nftokenOffer(cheri, env.seq(cheri)).key;
                 env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)),
                     token::Owner(becky),
                     Ter(createOfferTER));
@@ -1641,14 +1641,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                         break;
                 }
                 // becky buys the nft for 1 drop.
-                uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftAutoTrustID, drops(1)), token::Owner(alice));
                 env.close();
                 env(token::acceptBuyOffer(alice, beckyBuyOfferIndex));
                 env.close();
 
                 // becky sells the nft for AUD.
-                uint256 const beckySellOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken));
                 env.close();
                 env(token::acceptSellOffer(cheri, beckySellOfferIndex));
@@ -1658,7 +1658,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 BEAST_EXPECT(env.balance(alice, gwAUD) == gwAUD(10));
 
                 // becky buys the nft back for CAD.
-                uint256 const beckyBuyBackOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckyBuyBackOfferIndex =
+                    keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftAutoTrustID, gwCAD(50)), token::Owner(cheri));
                 env.close();
                 env(token::acceptBuyOffer(cheri, beckyBuyBackOfferIndex));
@@ -1678,7 +1679,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 env.close();
 
                 // alice sells the nft using AUD.
-                uint256 const aliceSellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+                uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
                 env(token::createOffer(alice, nftNoAutoTrustID, gwAUD(200)),
                     Txflags(tfSellNFToken));
                 env.close();
@@ -1695,7 +1696,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                     Txflags(tfSellNFToken),
                     Ter(tecNO_LINE));
                 env.close();
-                uint256 const cheriSellOfferIndex = keylet::nftoffer(cheri, env.seq(cheri)).key;
+                uint256 const cheriSellOfferIndex = keylet::nftokenOffer(cheri, env.seq(cheri)).key;
                 env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)),
                     Txflags(tfSellNFToken));
                 env.close();
@@ -1742,7 +1743,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 Ter(tefNFTOKEN_IS_NOT_TRANSFERABLE));
 
             // alice offers to sell the nft and becky accepts the offer.
-            uint256 const aliceSellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftAliceNoTransferID, XRP(20)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(becky, aliceSellOfferIndex));
@@ -1770,7 +1771,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // alice offers to buy the nft back from becky.  becky accepts
             // the offer.
-            uint256 const aliceBuyOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceBuyOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftAliceNoTransferID, XRP(22)), token::Owner(becky));
             env.close();
             env(token::acceptBuyOffer(becky, aliceBuyOfferIndex));
@@ -1826,7 +1827,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // minter successfully offers their nft for sale.
             BEAST_EXPECT(ownerCount(env, minter) == 1);
-            uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftMinterNoTransferID, XRP(22)), Txflags(tfSellNFToken));
             env.close();
             BEAST_EXPECT(ownerCount(env, minter) == 2);
@@ -1861,7 +1862,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // alice can create an offer to buy the nft.
             BEAST_EXPECT(ownerCount(env, alice) == 0);
-            uint256 const aliceBuyOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceBuyOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftMinterNoTransferID, XRP(25)), token::Owner(becky));
             env.close();
             BEAST_EXPECT(ownerCount(env, alice) == 1);
@@ -1876,7 +1877,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Now minter can create an offer to buy the nft.
             BEAST_EXPECT(ownerCount(env, minter) == 0);
-            uint256 const minterBuyOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftMinterNoTransferID, XRP(26)), token::Owner(becky));
             env.close();
             BEAST_EXPECT(ownerCount(env, minter) == 1);
@@ -1915,12 +1916,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(ownerCount(env, alice) == 1);
 
             // Both alice and becky can make offers for alice's nft.
-            uint256 const aliceSellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftAliceID, XRP(20)), Txflags(tfSellNFToken));
             env.close();
             BEAST_EXPECT(ownerCount(env, alice) == 2);
 
-            uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftAliceID, XRP(21)), token::Owner(alice));
             env.close();
             BEAST_EXPECT(ownerCount(env, alice) == 2);
@@ -1932,7 +1933,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(ownerCount(env, becky) == 2);
 
             // becky offers to sell the nft.
-            uint256 const beckySellOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftAliceID, XRP(22)), Txflags(tfSellNFToken));
             env.close();
             BEAST_EXPECT(ownerCount(env, alice) == 0);
@@ -1947,7 +1948,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(ownerCount(env, minter) == 1);
 
             // minter offers to sell the nft.
-            uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftAliceID, XRP(23)), Txflags(tfSellNFToken));
             env.close();
             BEAST_EXPECT(ownerCount(env, alice) == 0);
@@ -2029,7 +2030,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             // Becky buys the nft for XAU(10).  Check balances.
-            uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, gwXAU(10)), token::Owner(alice));
             env.close();
             BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
@@ -2041,7 +2042,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990));
 
             // becky sells nft to carol.  alice's balance should not change.
-            uint256 const beckySellOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, gwXAU(10)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(carol, beckySellOfferIndex));
@@ -2051,7 +2052,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990));
 
             // minter buys nft from carol.  alice's balance should not change.
-            uint256 const minterBuyOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, gwXAU(10)), token::Owner(carol));
             env.close();
             env(token::acceptBuyOffer(carol, minterBuyOfferIndex));
@@ -2063,7 +2064,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // minter sells the nft to alice.  gwXAU balances should finish
             // where they started.
-            uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, gwXAU(10)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(alice, minterSellOfferIndex));
@@ -2090,7 +2091,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             // Becky buys the nft for XAU(10).  Check balances.
-            uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, gwXAU(10)), token::Owner(alice));
             env.close();
             BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
@@ -2102,7 +2103,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990));
 
             // becky sells nft to carol.  alice's balance goes up.
-            uint256 const beckySellOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, gwXAU(10)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(carol, beckySellOfferIndex));
@@ -2113,7 +2114,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990));
 
             // minter buys nft from carol.  alice's balance goes up.
-            uint256 const minterBuyOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, gwXAU(10)), token::Owner(carol));
             env.close();
             env(token::acceptBuyOffer(carol, minterBuyOfferIndex));
@@ -2126,7 +2127,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // minter sells the nft to alice.  Because alice is part of the
             // transaction no transfer fee is removed.
-            uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, gwXAU(10)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(alice, minterSellOfferIndex));
@@ -2171,7 +2172,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             // Becky buys the nft for XAU(10).  Check balances.
-            uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, gwXAU(10)), token::Owner(alice));
             env.close();
             BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000));
@@ -2183,7 +2184,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990));
 
             // becky sells nft to minter.  alice's balance goes up.
-            uint256 const beckySellOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, gwXAU(100)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(minter, beckySellOfferIndex));
@@ -2194,7 +2195,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900));
 
             // carol buys nft from minter.  alice's balance goes up.
-            uint256 const carolBuyOfferIndex = keylet::nftoffer(carol, env.seq(carol)).key;
+            uint256 const carolBuyOfferIndex = keylet::nftokenOffer(carol, env.seq(carol)).key;
             env(token::createOffer(carol, nftID, gwXAU(10)), token::Owner(minter));
             env.close();
             env(token::acceptBuyOffer(minter, carolBuyOfferIndex));
@@ -2207,7 +2208,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // carol sells the nft to alice.  Because alice is part of the
             // transaction no transfer fee is removed.
-            uint256 const carolSellOfferIndex = keylet::nftoffer(carol, env.seq(carol)).key;
+            uint256 const carolSellOfferIndex = keylet::nftokenOffer(carol, env.seq(carol)).key;
             env(token::createOffer(carol, nftID, gwXAU(10)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(alice, carolSellOfferIndex));
@@ -2248,7 +2249,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // alice there should be no transfer fee.
             STAmount aliceBalance = env.balance(alice);
             STAmount minterBalance = env.balance(minter);
-            uint256 const minterBuyOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, XRP(1)), token::Owner(alice));
             env.close();
             env(token::acceptBuyOffer(alice, minterBuyOfferIndex));
@@ -2262,7 +2263,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // alice does not get any transfer fee.
             auto pmt = drops(50000);
             STAmount carolBalance = env.balance(carol);
-            uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, pmt), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(carol, minterSellOfferIndex));
@@ -2276,7 +2277,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // carol sells to becky. This is the smallest amount to pay for a
             // transfer that enables a transfer fee of 1 basis point.
             STAmount beckyBalance = env.balance(becky);
-            uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             pmt = drops(50001);
             env(token::createOffer(becky, nftID, pmt), token::Owner(carol));
             env.close();
@@ -2321,7 +2322,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // alice there should be no transfer fee.
             STAmount aliceBalance = env.balance(alice, gwXAU);
             STAmount minterBalance = env.balance(minter, gwXAU);
-            uint256 const minterBuyOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, tinyXAU), token::Owner(alice));
             env.close();
             env(token::acceptBuyOffer(alice, minterBuyOfferIndex));
@@ -2333,7 +2334,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // minter sells to carol.
             STAmount carolBalance = env.balance(carol, gwXAU);
-            uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftID, tinyXAU), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(carol, minterSellOfferIndex));
@@ -2351,7 +2352,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             STAmount const cheapNFT(gwXAU, STAmount::kMinValue, STAmount::kMinOffset + 5);
 
             STAmount beckyBalance = env.balance(becky, gwXAU);
-            uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftID, cheapNFT), token::Owner(carol));
             env.close();
             env(token::acceptBuyOffer(carol, beckyBuyOfferIndex));
@@ -2581,22 +2582,22 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Test how adding a Destination field to an offer affects permissions
         // for canceling offers.
         {
-            uint256 const offerMinterToIssuer = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToIssuer = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(issuer),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerMinterToBuyer = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToBuyer = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(buyer),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerIssuerToMinter = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const offerIssuerToMinter = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftokenID, drops(1)),
                 token::Owner(minter),
                 token::Destination(minter));
 
-            uint256 const offerIssuerToBuyer = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const offerIssuerToBuyer = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftokenID, drops(1)),
                 token::Owner(minter),
                 token::Destination(buyer));
@@ -2637,7 +2638,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Test how adding a Destination field to a sell offer affects
         // accepting that offer.
         {
-            uint256 const offerMinterSellsToBuyer = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterSellsToBuyer =
+                keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(buyer),
                 Txflags(tfSellNFToken));
@@ -2665,7 +2667,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Test how adding a Destination field to a buy offer affects
         // accepting that offer.
         {
-            uint256 const offerMinterBuysFromBuyer = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterBuysFromBuyer =
+                keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Owner(buyer),
                 token::Destination(buyer));
@@ -2692,7 +2695,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // If a destination other than the NFToken owner is set, that
             // destination must act as a broker.  The NFToken owner may not
             // simply accept the offer.
-            uint256 const offerBuyerBuysFromMinter = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerBuyerBuysFromMinter =
+                keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID, drops(1)),
                 token::Owner(minter),
                 token::Destination(broker));
@@ -2715,12 +2719,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Show that a sell offer's Destination can broker that sell offer
         // to another account.
         {
-            uint256 const offerMinterToBroker = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToBroker = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(broker),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerBuyerToMinter = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerBuyerToMinter = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID, drops(1)), token::Owner(minter));
 
             env.close();
@@ -2752,15 +2756,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Destination doesn't match, but can complete if the Destination
         // does match.
         {
-            uint256 const offerBuyerToMinter = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerBuyerToMinter = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID, drops(1)),
                 token::Destination(minter),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerMinterToBuyer = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToBuyer = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)), token::Owner(buyer));
 
-            uint256 const offerIssuerToBuyer = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const offerIssuerToBuyer = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftokenID, drops(1)), token::Owner(buyer));
 
             env.close();
@@ -2808,12 +2812,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Show that if a buy and a sell offer both have the same destination,
         // then that destination can broker the offers.
         {
-            uint256 const offerMinterToBroker = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToBroker = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(broker),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerBuyerToBroker = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerBuyerToBroker = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID, drops(1)),
                 token::Owner(minter),
                 token::Destination(broker));
@@ -2883,7 +2887,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // create offer (allowed now) then cancel
         {
-            uint256 const offerIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
 
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(buyer),
@@ -2896,7 +2900,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // create offer, enable flag, then cancel
         {
-            uint256 const offerIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
 
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(buyer),
@@ -2915,7 +2919,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // create offer then transfer
         {
-            uint256 const offerIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
 
             env(token::createOffer(minter, nftokenID, drops(1)),
                 token::Destination(buyer),
@@ -3002,23 +3006,23 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             std::uint32_t const expiration = lastClose(env) + 25;
 
-            uint256 const offerMinterToIssuer = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToIssuer = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID0, drops(1)),
                 token::Destination(issuer),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerMinterToAnyone = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offerMinterToAnyone = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID0, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
 
-            uint256 const offerIssuerToMinter = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const offerIssuerToMinter = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftokenID0, drops(1)),
                 token::Owner(minter),
                 token::Expiration(expiration));
 
-            uint256 const offerBuyerToMinter = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerBuyerToMinter = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, drops(1)),
                 token::Owner(minter),
                 token::Expiration(expiration));
@@ -3078,13 +3082,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             std::uint32_t const expiration = lastClose(env) + 25;
 
-            uint256 const offer0 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offer0 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID0, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
             minterCount++;
 
-            uint256 const offer1 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const offer1 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID1, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
@@ -3149,7 +3153,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Transfer nftokenID0 back to minter so we start the next test in
             // a simple place.
-            uint256 const offerSellBack = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, XRP(0)),
                 Txflags(tfSellNFToken),
                 token::Destination(minter));
@@ -3168,13 +3172,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             std::uint32_t const expiration = lastClose(env) + 25;
 
-            uint256 const offer0 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, drops(1)),
                 token::Owner(minter),
                 token::Expiration(expiration));
             buyerCount++;
 
-            uint256 const offer1 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID1, drops(1)),
                 token::Owner(minter),
                 token::Expiration(expiration));
@@ -3237,7 +3241,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Transfer nftokenID0 back to minter so we start the next test in
             // a simple place.
-            uint256 const offerSellBack = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, XRP(0)),
                 Txflags(tfSellNFToken),
                 token::Destination(minter));
@@ -3256,23 +3260,23 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             std::uint32_t const expiration = lastClose(env) + 25;
 
-            uint256 const sellOffer0 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const sellOffer0 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID0, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
             minterCount++;
 
-            uint256 const sellOffer1 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const sellOffer1 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID1, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
             minterCount++;
 
-            uint256 const buyOffer0 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyOffer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, drops(1)), token::Owner(minter));
             buyerCount++;
 
-            uint256 const buyOffer1 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyOffer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID1, drops(1)), token::Owner(minter));
             buyerCount++;
 
@@ -3331,7 +3335,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Transfer nftokenID0 back to minter so we start the next test in
             // a simple place.
-            uint256 const offerSellBack = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, XRP(0)),
                 Txflags(tfSellNFToken),
                 token::Destination(minter));
@@ -3350,18 +3354,18 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             std::uint32_t const expiration = lastClose(env) + 25;
 
-            uint256 const sellOffer0 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const sellOffer0 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID0, drops(1)), Txflags(tfSellNFToken));
 
-            uint256 const sellOffer1 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const sellOffer1 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID1, drops(1)), Txflags(tfSellNFToken));
 
-            uint256 const buyOffer0 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyOffer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, drops(1)),
                 token::Expiration(expiration),
                 token::Owner(minter));
 
-            uint256 const buyOffer1 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyOffer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID1, drops(1)),
                 token::Expiration(expiration),
                 token::Owner(minter));
@@ -3412,7 +3416,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Transfer nftokenID0 back to minter so we start the next test in
             // a simple place.
-            uint256 const offerSellBack = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, XRP(0)),
                 Txflags(tfSellNFToken),
                 token::Destination(minter));
@@ -3431,22 +3435,22 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             std::uint32_t const expiration = lastClose(env) + 25;
 
-            uint256 const sellOffer0 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const sellOffer0 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID0, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
 
-            uint256 const sellOffer1 = keylet::nftoffer(minter, env.seq(minter)).key;
+            uint256 const sellOffer1 = keylet::nftokenOffer(minter, env.seq(minter)).key;
             env(token::createOffer(minter, nftokenID1, drops(1)),
                 token::Expiration(expiration),
                 Txflags(tfSellNFToken));
 
-            uint256 const buyOffer0 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyOffer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, drops(1)),
                 token::Expiration(expiration),
                 token::Owner(minter));
 
-            uint256 const buyOffer1 = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyOffer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID1, drops(1)),
                 token::Expiration(expiration),
                 token::Owner(minter));
@@ -3488,7 +3492,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Transfer nftokenID0 back to minter so we start the next test in
             // a simple place.
-            uint256 const offerSellBack = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftokenID0, XRP(0)),
                 Txflags(tfSellNFToken),
                 token::Destination(minter));
@@ -3526,7 +3530,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         env.close();
 
         // Anyone can cancel an expired offer.
-        uint256 const expiredOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const expiredOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
 
         env(token::createOffer(alice, nftokenID, XRP(1000)),
             Txflags(tfSellNFToken),
@@ -3548,7 +3552,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // Create a couple of offers with a destination.  Those offers
         // should be cancellable by the creator and the destination.
-        uint256 const dest1OfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const dest1OfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
 
         env(token::createOffer(alice, nftokenID, XRP(1000)),
             token::Destination(becky),
@@ -3566,7 +3570,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(ownerCount(env, alice) == 1);
 
         // alice can cancel her own offer, even if becky is the destination.
-        uint256 const dest2OfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const dest2OfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
 
         env(token::createOffer(alice, nftokenID, XRP(1000)),
             token::Destination(becky),
@@ -3585,7 +3589,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         env(token::mint(minter, 0), token::Issuer(alice), Txflags(tfTransferable));
         env.close();
 
-        uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+        uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
 
         env(token::createOffer(minter, mintersNFTokenID, XRP(1000)), Txflags(tfSellNFToken));
         env.close();
@@ -3643,7 +3647,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env(token::mint(nftAcct, 0), token::Uri(uri), Txflags(tfTransferable));
             env.close();
 
-            offerIndexes.push_back(keylet::nftoffer(offerAcct, env.seq(offerAcct)).key);
+            offerIndexes.push_back(keylet::nftokenOffer(offerAcct, env.seq(offerAcct)).key);
             env(token::createOffer(offerAcct, nftokenID, drops(1)),
                 token::Owner(nftAcct),
                 token::Expiration(lastClose(env) + 5));
@@ -3656,7 +3660,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // All offers should be in the ledger.
         for (uint256 const& offerIndex : offerIndexes)
         {
-            BEAST_EXPECT(env.le(keylet::nftoffer(offerIndex)));
+            BEAST_EXPECT(env.le(keylet::nftokenOffer(offerIndex)));
         }
 
         // alice attempts to cancel all of the expired offers.  There is one
@@ -3669,7 +3673,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         env.close();
 
         // Verify that offer is gone from the ledger.
-        BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndexes.back())));
+        BEAST_EXPECT(!env.le(keylet::nftokenOffer(offerIndexes.back())));
         offerIndexes.pop_back();
 
         // But alice adds a sell offer to the list...
@@ -3678,7 +3682,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env(token::mint(alice, 0), token::Uri(uri), Txflags(tfTransferable));
             env.close();
 
-            offerIndexes.push_back(keylet::nftoffer(alice, env.seq(alice)).key);
+            offerIndexes.push_back(keylet::nftokenOffer(alice, env.seq(alice)).key);
             env(token::createOffer(alice, nftokenID, drops(1)), Txflags(tfSellNFToken));
             env.close();
 
@@ -3708,7 +3712,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         // Verify that remaining offers are gone from the ledger.
         for (uint256 const& offerIndex : offerIndexes)
         {
-            BEAST_EXPECT(!env.le(keylet::nftoffer(offerIndex)));
+            BEAST_EXPECT(!env.le(keylet::nftokenOffer(offerIndex)));
         }
     }
 
@@ -3789,13 +3793,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT();
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken));
                 env.close();
 
                 // buyer creates their offer.  Note: a buy offer can never
                 // offer zero.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter));
                 env.close();
 
@@ -3831,13 +3835,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT();
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken));
                 env.close();
 
                 // buyer creates their offer.  Note: a buy offer can never
                 // offer zero.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter));
                 env.close();
 
@@ -3880,13 +3884,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT(kMaxTransferFee);
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken));
                 env.close();
 
                 // buyer creates their offer.  Note: a buy offer can never
                 // offer zero.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter));
                 env.close();
 
@@ -3922,13 +3926,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT(kMaxTransferFee);
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken));
                 env.close();
 
                 // buyer creates their offer.  Note: a buy offer can never
                 // offer zero.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter));
                 env.close();
 
@@ -3995,14 +3999,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT();
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, gwXAU(1000)), Txflags(tfSellNFToken));
                 env.close();
 
                 {
                     // buyer creates an offer for more XAU than they currently
                     // own.
-                    uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                    uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                     env(token::createOffer(buyer, nftID, gwXAU(1001)), token::Owner(minter));
                     env.close();
 
@@ -4019,7 +4023,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 {
                     // buyer creates an offer for less that what minter is
                     // asking.
-                    uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                    uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                     env(token::createOffer(buyer, nftID, gwXAU(999)), token::Owner(minter));
                     env.close();
 
@@ -4035,7 +4039,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 }
 
                 // buyer creates a large enough offer.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter));
                 env.close();
 
@@ -4072,13 +4076,13 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT(kMaxTransferFee);
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, gwXAU(900)), Txflags(tfSellNFToken));
                 env.close();
                 {
                     // buyer creates an offer for more XAU than they currently
                     // own.
-                    uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                    uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                     env(token::createOffer(buyer, nftID, gwXAU(1001)), token::Owner(minter));
                     env.close();
 
@@ -4095,7 +4099,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 {
                     // buyer creates an offer for less that what minter is
                     // asking.
-                    uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                    uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                     env(token::createOffer(buyer, nftID, gwXAU(899)), token::Owner(minter));
                     env.close();
 
@@ -4110,7 +4114,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                     env.close();
                 }
                 // buyer creates a large enough offer.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter));
                 env.close();
 
@@ -4150,12 +4154,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT(kMaxTransferFee / 2);  // 25%
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, gwXAU(900)), Txflags(tfSellNFToken));
                 env.close();
 
                 // buyer creates a large enough offer.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter));
                 env.close();
 
@@ -4187,12 +4191,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 uint256 const nftID = mintNFT(kMaxTransferFee / 2);  // 25%
 
                 // minter creates their offer.
-                uint256 const minterOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key;
+                uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key;
                 env(token::createOffer(minter, nftID, gwXAU(900)), Txflags(tfSellNFToken));
                 env.close();
 
                 // buyer creates a large enough offer.
-                uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+                uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
                 env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter));
                 env.close();
 
@@ -4242,9 +4246,9 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(nftCount(env, buyer2) == 0);
 
         // Both buyer1 and buyer2 create buy offers for nftId.
-        uint256 const buyer1OfferIndex = keylet::nftoffer(buyer1, env.seq(buyer1)).key;
+        uint256 const buyer1OfferIndex = keylet::nftokenOffer(buyer1, env.seq(buyer1)).key;
         env(token::createOffer(buyer1, nftId, XRP(100)), token::Owner(issuer));
-        uint256 const buyer2OfferIndex = keylet::nftoffer(buyer2, env.seq(buyer2)).key;
+        uint256 const buyer2OfferIndex = keylet::nftokenOffer(buyer2, env.seq(buyer2)).key;
         env(token::createOffer(buyer2, nftId, XRP(100)), token::Owner(issuer));
         env.close();
 
@@ -4332,7 +4336,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // NFTokenCreateOffer
         BEAST_EXPECT(ownerCount(env, buyer) == 10);
-        uint256 const offerIndex0 = keylet::nftoffer(buyer, buyerTicketSeq).key;
+        uint256 const offerIndex0 = keylet::nftokenOffer(buyer, buyerTicketSeq).key;
         env(token::createOffer(buyer, nftId, XRP(1)),
             token::Owner(issuer),
             ticket::Use(buyerTicketSeq++));
@@ -4347,7 +4351,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         BEAST_EXPECT(ticketCount(env, buyer) == 8);
 
         // NFTokenCreateOffer.  buyer tries again.
-        uint256 const offerIndex1 = keylet::nftoffer(buyer, buyerTicketSeq).key;
+        uint256 const offerIndex1 = keylet::nftokenOffer(buyer, buyerTicketSeq).key;
         env(token::createOffer(buyer, nftId, XRP(2)),
             token::Owner(issuer),
             ticket::Use(buyerTicketSeq++));
@@ -4424,7 +4428,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         env(token::createOffer(becky, nftId, XRP(2)), token::Owner(minter));
         env.close();
 
-        uint256 const carlaOfferIndex = keylet::nftoffer(carla, env.seq(carla)).key;
+        uint256 const carlaOfferIndex = keylet::nftokenOffer(carla, env.seq(carla)).key;
         env(token::createOffer(carla, nftId, XRP(3)), token::Owner(minter));
         env.close();
 
@@ -4702,25 +4706,25 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             TER const offerCreateTER = temBAD_AMOUNT;
 
             // Make offers with negative amounts for the NFTs
-            uint256 const sellNegXrpOfferIndex = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const sellNegXrpOfferIndex = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftID0, XRP(-2)),
                 Txflags(tfSellNFToken),
                 Ter(offerCreateTER));
             env.close();
 
-            uint256 const sellNegIouOfferIndex = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const sellNegIouOfferIndex = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftID1, gwXAU(-2)),
                 Txflags(tfSellNFToken),
                 Ter(offerCreateTER));
             env.close();
 
-            uint256 const buyNegXrpOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyNegXrpOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftID0, XRP(-1)),
                 token::Owner(issuer),
                 Ter(offerCreateTER));
             env.close();
 
-            uint256 const buyNegIouOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const buyNegIouOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::createOffer(buyer, nftID1, gwXAU(-1)),
                 token::Owner(issuer),
                 Ter(offerCreateTER));
@@ -4883,7 +4887,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                                       uint256 const& nftID,
                                       STAmount const& amount,
                                       std::optional const terCode = {}) {
-                uint256 const offerID = keylet::nftoffer(offerer, env.seq(offerer)).key;
+                uint256 const offerID = keylet::nftokenOffer(offerer, env.seq(offerer)).key;
                 env(token::createOffer(offerer, nftID, amount),
                     token::Owner(owner),
                     terCode ? Ter(*terCode) : Ter(static_cast(tesSUCCESS)));
@@ -4896,7 +4900,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                                        uint256 const& nftID,
                                        STAmount const& amount,
                                        std::optional const terCode = {}) {
-                uint256 const offerID = keylet::nftoffer(offerer, env.seq(offerer)).key;
+                uint256 const offerID = keylet::nftokenOffer(offerer, env.seq(offerer)).key;
                 env(token::createOffer(offerer, nftID, amount),
                     Txflags(tfSellNFToken),
                     terCode ? Ter(*terCode) : Ter(static_cast(tesSUCCESS)));
@@ -5409,10 +5413,10 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // Bob creates a buy offer for 5 XRP.  Alice creates a sell offer
         // for 0 XRP.
-        uint256 const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
+        uint256 const bobBuyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key;
         env(token::createOffer(bob, nftId, XRP(5)), token::Owner(alice));
 
-        uint256 const aliceSellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key;
+        uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key;
         env(token::createOffer(alice, nftId, XRP(0)),
             token::Destination(bob),
             Txflags(tfSellNFToken));
@@ -5423,10 +5427,10 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         env.close();
 
         // Note that bob still has a buy offer on the books.
-        BEAST_EXPECT(env.le(keylet::nftoffer(bobBuyOfferIndex)));
+        BEAST_EXPECT(env.le(keylet::nftokenOffer(bobBuyOfferIndex)));
 
         // Bob creates a sell offer for the gift NFT from alice.
-        uint256 const bobSellOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
+        uint256 const bobSellOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key;
         env(token::createOffer(bob, nftId, XRP(4)), Txflags(tfSellNFToken));
         env.close();
 
@@ -6043,7 +6047,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 token::Amount(XRP(10)),
                 token::Destination(buyer),
                 token::Expiration(lastClose(env) + 25));
-            uint256 const offerAliceSellsToBuyer = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const offerAliceSellsToBuyer = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::cancelOffer(alice, {offerAliceSellsToBuyer}));
             env.close();
 
@@ -6052,7 +6056,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
                 token::Amount(XRP(10)),
                 token::Destination(alice),
                 token::Expiration(lastClose(env) + 25));
-            uint256 const offerBuyerSellsToAlice = keylet::nftoffer(buyer, env.seq(buyer)).key;
+            uint256 const offerBuyerSellsToAlice = keylet::nftokenOffer(buyer, env.seq(buyer)).key;
             env(token::cancelOffer(alice, {offerBuyerSellsToAlice}));
             env.close();
 
@@ -6203,12 +6207,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // Alice creates one sell offer for each NFT
             // Verify the offer indexes are correct in the NFTokenCreateOffer tx
             // meta
-            uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId1, drops(1)), Txflags(tfSellNFToken));
             env.close();
             verifyNFTokenOfferID(aliceOfferIndex1);
 
-            uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId2, drops(1)), Txflags(tfSellNFToken));
             env.close();
             verifyNFTokenOfferID(aliceOfferIndex2);
@@ -6222,7 +6226,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // Bobs creates a buy offer for nftId1
             // Verify the offer id is correct in the NFTokenCreateOffer tx meta
-            auto const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
+            auto const bobBuyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key;
             env(token::createOffer(bob, nftId1, drops(1)), token::Owner(alice));
             env.close();
             verifyNFTokenOfferID(bobBuyOfferIndex);
@@ -6243,7 +6247,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             verifyNFTokenID(nftId);
 
             // Alice creates sell offer and set broker as destination
-            uint256 const offerAliceToBroker = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const offerAliceToBroker = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, drops(1)),
                 token::Destination(broker),
                 Txflags(tfSellNFToken));
@@ -6251,7 +6255,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             verifyNFTokenOfferID(offerAliceToBroker);
 
             // Bob creates buy offer
-            uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key;
+            uint256 const offerBobToBroker = keylet::nftokenOffer(bob, env.seq(bob)).key;
             env(token::createOffer(bob, nftId, drops(1)), token::Owner(alice));
             env.close();
             verifyNFTokenOfferID(offerBobToBroker);
@@ -6272,12 +6276,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             verifyNFTokenID(nftId);
 
             // Alice creates 2 sell offers for the same NFT
-            uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
             env.close();
             verifyNFTokenOfferID(aliceOfferIndex1);
 
-            uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
             env.close();
             verifyNFTokenOfferID(aliceOfferIndex2);
@@ -6291,7 +6295,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         if (features[featureNFTokenMintOffer])
         {
-            uint256 const aliceMintWithOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceMintWithOfferIndex1 =
+                keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::mint(alice), token::Amount(XRP(0)));
             env.close();
             verifyNFTokenOfferID(aliceMintWithOfferIndex1);
@@ -6314,7 +6319,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             // acct makes an sell offer
-            uint256 const sellOfferIndex = keylet::nftoffer(acct, env.seq(acct)).key;
+            uint256 const sellOfferIndex = keylet::nftokenOffer(acct, env.seq(acct)).key;
             env(token::createOffer(acct, nftId, amt), Txflags(tfSellNFToken));
             env.close();
 
@@ -6519,7 +6524,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             // Bob makes a buy offer for 1 XRP
-            auto const buyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
+            auto const buyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key;
             env(token::createOffer(bob, nftId, XRP(1)), token::Owner(alice));
             env.close();
 
@@ -6567,14 +6572,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
 
             // Alice creates sell offer and set broker as destination
-            uint256 const offerAliceToBroker = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const offerAliceToBroker = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, XRP(1)),
                 token::Destination(broker),
                 Txflags(tfSellNFToken));
             env.close();
 
             // Bob creates buy offer
-            uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key;
+            uint256 const offerBobToBroker = keylet::nftokenOffer(bob, env.seq(bob)).key;
             env(token::createOffer(bob, nftId, XRP(1)), token::Owner(alice));
             env.close();
 
@@ -6668,10 +6673,10 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // becky buys the nfts for 1 drop each.
             {
-                uint256 const beckyBuyOfferIndex1 = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckyBuyOfferIndex1 = keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftAutoTrustID, drops(1)), token::Owner(issuer));
 
-                uint256 const beckyBuyOfferIndex2 = keylet::nftoffer(becky, env.seq(becky)).key;
+                uint256 const beckyBuyOfferIndex2 = keylet::nftokenOffer(becky, env.seq(becky)).key;
                 env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), token::Owner(issuer));
 
                 env.close();
@@ -6681,7 +6686,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             }
 
             // becky creates offers to sell the nfts for AUD.
-            uint256 const beckyAutoTrustOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyAutoTrustOfferIndex =
+                keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken));
             env.close();
 
@@ -6699,7 +6705,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.close();
             BEAST_EXPECT(ownerCount(env, issuer) == 1);
 
-            uint256 const beckyNoAutoTrustOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyNoAutoTrustOfferIndex =
+                keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken));
             env.close();
 
@@ -6823,10 +6830,10 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
         // becky buys the nfts for 1 drop each.
         {
-            uint256 const beckyBuyOfferIndex1 = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex1 = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftAutoTrustID, drops(1)), token::Owner(issuer));
 
-            uint256 const beckyBuyOfferIndex2 = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyBuyOfferIndex2 = keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), token::Owner(issuer));
 
             env.close();
@@ -6853,7 +6860,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
 
             // However if the NFToken has the tfTrustLine flag set,
             // then becky can create the offer.
-            uint256 const beckyAutoTrustOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyAutoTrustOfferIndex =
+                keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftAutoTrustID, isISU(100)), Txflags(tfSellNFToken));
             env.close();
 
@@ -6870,10 +6878,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         {
             // With featureNFTokenMintOffer things go better.
             // becky creates offers to sell the nfts for ISU.
-            uint256 const beckyNoAutoTrustOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyNoAutoTrustOfferIndex =
+                keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftNoAutoTrustID, isISU(100)), Txflags(tfSellNFToken));
             env.close();
-            uint256 const beckyAutoTrustOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key;
+            uint256 const beckyAutoTrustOfferIndex =
+                keylet::nftokenOffer(becky, env.seq(becky)).key;
             env(token::createOffer(becky, nftAutoTrustID, isISU(100)), Txflags(tfSellNFToken));
             env.close();
 
@@ -7107,7 +7117,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             checkURI(issuer, "uri", __LINE__);
 
             // Account != Owner
-            uint256 const offerID = keylet::nftoffer(issuer, env.seq(issuer)).key;
+            uint256 const offerID = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
             env(token::createOffer(issuer, nftId, XRP(0)), Txflags(tfSellNFToken));
             env.close();
             env(token::acceptSellOffer(alice, offerID));
diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp
index 1a2f0b2b12..804bd9b86c 100644
--- a/src/test/app/Offer_test.cpp
+++ b/src/test/app/Offer_test.cpp
@@ -2196,7 +2196,7 @@ public:
         jtx::PrettyAmount const& expectBalance)
     {
         Issue const& issue = expectBalance.value().get();
-        auto const sleTrust = env.le(keylet::line(account.id(), issue));
+        auto const sleTrust = env.le(keylet::trustLine(account.id(), issue));
         BEAST_EXPECT(sleTrust);
         if (sleTrust)
         {
@@ -2363,7 +2363,7 @@ public:
                 else
                 {
                     // Verify that no trustline was created.
-                    auto const sleTrust = env.le(keylet::line(acct, usd));
+                    auto const sleTrust = env.le(keylet::trustLine(acct, usd));
                     BEAST_EXPECT(!sleTrust);
                 }
             }
@@ -2548,8 +2548,8 @@ public:
         env.require(offers(bob, 0));
 
         // The two trustlines that were generated by offers should be gone.
-        BEAST_EXPECT(!env.le(keylet::line(alice.id(), eur)));
-        BEAST_EXPECT(!env.le(keylet::line(bob.id(), usd)));
+        BEAST_EXPECT(!env.le(keylet::trustLine(alice.id(), eur)));
+        BEAST_EXPECT(!env.le(keylet::trustLine(bob.id(), usd)));
 
         // Make two more offers that leave one of the offers non-dry. We
         // need to properly sequence the transactions:
@@ -4484,7 +4484,7 @@ public:
                                   jtx::Account const& src,
                                   jtx::Account const& dst,
                                   Currency const& cur) -> bool {
-            return bool(env.le(keylet::line(src, dst, cur)));
+            return bool(env.le(keylet::trustLine(src, dst, cur)));
         };
 
         Account const alice("alice");
diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp
index 4cfe938798..8f19a419a0 100644
--- a/src/test/app/Path_test.cpp
+++ b/src/test/app/Path_test.cpp
@@ -880,7 +880,7 @@ public:
             })",
             jv);
 
-        auto const jvL = env.le(keylet::line(Account("bob").id(), Account("alice")["USD"]))
+        auto const jvL = env.le(keylet::trustLine(Account("bob").id(), Account("alice")["USD"]))
                              ->getJson(JsonOptions::Values::None);
         for (auto it = jv.begin(); it != jv.end(); ++it)
             BEAST_EXPECT(*it == jvL[it.memberName()]);
@@ -922,14 +922,15 @@ public:
             })",
             jv);
 
-        auto const jvL = env.le(keylet::line(Account("bob").id(), Account("alice")["USD"]))
+        auto const jvL = env.le(keylet::trustLine(Account("bob").id(), Account("alice")["USD"]))
                              ->getJson(JsonOptions::Values::None);
         for (auto it = jv.begin(); it != jv.end(); ++it)
             BEAST_EXPECT(*it == jvL[it.memberName()]);
 
         env.trust(Account("bob")["USD"](0), "alice");
         env.trust(Account("alice")["USD"](0), "bob");
-        BEAST_EXPECT(env.le(keylet::line(Account("bob").id(), Account("alice")["USD"])) == nullptr);
+        BEAST_EXPECT(
+            env.le(keylet::trustLine(Account("bob").id(), Account("alice")["USD"])) == nullptr);
     }
 
     void
@@ -972,13 +973,14 @@ public:
             })",
             jv);
 
-        auto const jvL = env.le(keylet::line(Account("alice").id(), Account("bob")["USD"]))
+        auto const jvL = env.le(keylet::trustLine(Account("alice").id(), Account("bob")["USD"]))
                              ->getJson(JsonOptions::Values::None);
         for (auto it = jv.begin(); it != jv.end(); ++it)
             BEAST_EXPECT(*it == jvL[it.memberName()]);
 
         env(pay("alice", "bob", Account("alice")["USD"](50)));
-        BEAST_EXPECT(env.le(keylet::line(Account("alice").id(), Account("bob")["USD"])) == nullptr);
+        BEAST_EXPECT(
+            env.le(keylet::trustLine(Account("alice").id(), Account("bob")["USD"])) == nullptr);
     }
 
     void
diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp
index bb2d9636e6..fd2c7790d5 100644
--- a/src/test/app/PayChan_test.cpp
+++ b/src/test/app/PayChan_test.cpp
@@ -62,7 +62,7 @@ struct PayChan_test : public beast::unit_test::Suite
         auto const sle = view.read(keylet::account(account));
         if (!sle)
             return {};
-        auto const k = keylet::payChan(account, dst, (*sle)[sfSequence] - 1);
+        auto const k = keylet::payChannel(account, dst, (*sle)[sfSequence] - 1);
         return {k.key, view.read(k)};
     }
 
diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp
index 471c641f36..1f9290de63 100644
--- a/src/test/app/PayStrand_test.cpp
+++ b/src/test/app/PayStrand_test.cpp
@@ -80,7 +80,7 @@ getTrustFlag(
     Currency const& cur,
     TrustFlag flag)
 {
-    if (auto sle = env.le(keylet::line(src, dst, cur)))
+    if (auto sle = env.le(keylet::trustLine(src, dst, cur)))
     {
         auto const useHigh = src.id() > dst.id();
         return sle->isFlag(trustFlag(flag, useHigh));
@@ -454,7 +454,7 @@ struct ExistingElementPool
                 for (auto const& c : currencies)
                 {
                     // Line balance
-                    auto const lk = keylet::line(*ai1, *ai2, c);
+                    auto const lk = keylet::trustLine(*ai1, *ai2, c);
                     auto const b1 = lineBalance(v1, lk);
                     auto const b2 = lineBalance(v2, lk);
                     if (b1 != b2)
diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp
index 51ca321f7e..8578b8a9ba 100644
--- a/src/test/app/PermissionedDEX_test.cpp
+++ b/src/test/app/PermissionedDEX_test.cpp
@@ -144,7 +144,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite
     static uint256
     getBookDirKey(Book const& book, STAmount const& takerPays, STAmount const& takerGets)
     {
-        return keylet::quality(keylet::kBook(book), getRate(takerGets, takerPays)).key;
+        return keylet::quality(keylet::book(book), getRate(takerGets, takerPays)).key;
     }
 
     static std::optional
diff --git a/src/test/app/RCLValidations_test.cpp b/src/test/app/RCLValidations_test.cpp
index c3b0ddea9c..aaf84225a7 100644
--- a/src/test/app/RCLValidations_test.cpp
+++ b/src/test/app/RCLValidations_test.cpp
@@ -97,7 +97,7 @@ class RCLValidations_test : public beast::unit_test::Suite
             auto next = std::make_shared(*prev, env.app().getTimeKeeper().closeTime());
             // Force a different hash on the first iteration
             next->updateSkipList();
-            BEAST_EXPECT(next->read(keylet::fees()));
+            BEAST_EXPECT(next->read(keylet::feeSettings()));
             if (forceHash)
             {
                 next->setImmutable();
diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp
index df0c6fe5d0..57526d0a0c 100644
--- a/src/test/app/SetAuth_test.cpp
+++ b/src/test/app/SetAuth_test.cpp
@@ -51,7 +51,7 @@ struct SetAuth_test : public beast::unit_test::Suite
         env(fset(gw, asfRequireAuth));
         env.close();
         env(auth(gw, "alice", "USD"));
-        BEAST_EXPECT(env.le(keylet::line(Account("alice").id(), gw.id(), usd.currency)));
+        BEAST_EXPECT(env.le(keylet::trustLine(Account("alice").id(), gw.id(), usd.currency)));
         env(trust("alice", usd(1000)));
         env(trust("bob", usd(1000)));
         env(pay(gw, "alice", usd(100)));
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index 065b6b0044..4643bc8555 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -113,7 +113,7 @@ class Vault_test : public beast::unit_test::Suite
                 {
                     BEAST_EXPECT(vault->at(sfScale) == 0);
                 }
-                auto const shares = env.le(keylet::mptIssuance(vault->at(sfShareMPTID)));
+                auto const shares = env.le(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
                 BEAST_EXPECT(shares != nullptr);
                 if (!asset.integral())
                 {
@@ -1692,7 +1692,7 @@ class Vault_test : public beast::unit_test::Suite
             auto v = env.le(keylet);
             BEAST_EXPECT(v);
             MPTID const share = (*v)[sfShareMPTID];
-            auto issuance = env.le(keylet::mptIssuance(share));
+            auto issuance = env.le(keylet::mptokenIssuance(share));
             BEAST_EXPECT(issuance);
             Number const outstandingShares = issuance->at(sfOutstandingAmount);
             BEAST_EXPECT(outstandingShares == 100);
@@ -2794,7 +2794,7 @@ class Vault_test : public beast::unit_test::Suite
             env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
             env.close();
 
-            auto trustline = env.le(keylet::line(owner, asset.raw().get()));
+            auto trustline = env.le(keylet::trustLine(owner, asset.raw().get()));
             BEAST_EXPECT(trustline == nullptr);
 
             // Withdraw without trust line, will succeed
@@ -2977,7 +2977,7 @@ class Vault_test : public beast::unit_test::Suite
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
                 env.close();
 
-                auto trustline = env.le(keylet::line(owner, asset.raw().get()));
+                auto trustline = env.le(keylet::trustLine(owner, asset.raw().get()));
                 BEAST_EXPECT(trustline == nullptr);
 
                 env(ticket::create(owner, 1));
@@ -3405,7 +3405,7 @@ class Vault_test : public beast::unit_test::Suite
             return {vault->at(sfAccount), vault->at(sfShareMPTID)};
         }();
         BEAST_EXPECT(env.le(keylet::account(vaultAccount)));
-        BEAST_EXPECT(env.le(keylet::mptIssuance(issuanceId)));
+        BEAST_EXPECT(env.le(keylet::mptokenIssuance(issuanceId)));
         PrettyAsset const shares{issuanceId};
 
         {
@@ -3559,7 +3559,7 @@ class Vault_test : public beast::unit_test::Suite
                         auto vault = sb.peek(keylet::vault(keylet.key));
                         if (!BEAST_EXPECT(vault))
                             return false;
-                        auto shares = sb.peek(keylet::mptIssuance(vault->at(sfShareMPTID)));
+                        auto shares = sb.peek(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
                         if (!BEAST_EXPECT(shares))
                             return false;
                         if (fn(*vault, *shares))
@@ -4308,7 +4308,7 @@ class Vault_test : public beast::unit_test::Suite
             BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
 
             // Create a loan broker backed by this vault
-            auto const brokerKeylet = keylet::loanbroker(d.owner.id(), env.seq(d.owner));
+            auto const brokerKeylet = keylet::loanBroker(d.owner.id(), env.seq(d.owner));
             env(set(d.owner, d.keylet.key));
             env.close();
 
@@ -4783,7 +4783,7 @@ class Vault_test : public beast::unit_test::Suite
             auto const sleVault = env.le(vaultKeylet);
             BEAST_EXPECT(sleVault != nullptr);
 
-            auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
             BEAST_EXPECT(sleIssuance != nullptr);
 
             return sleIssuance->at(sfOutstandingAmount);
@@ -4822,7 +4822,7 @@ class Vault_test : public beast::unit_test::Suite
             env.close();
 
             auto const& sharesAvailable = vaultShareBalance(vaultKeylet);
-            auto const& brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+            auto const& brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
 
             env(set(owner, vaultKeylet.key));
             env.close();
@@ -5215,7 +5215,7 @@ class Vault_test : public beast::unit_test::Suite
                 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
 
                 // Create a loan broker backed by this vault
-                auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+                auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
                 env(set(owner, vaultKeylet.key));
                 env.close();
 
@@ -5273,7 +5273,7 @@ class Vault_test : public beast::unit_test::Suite
                 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
 
                 // Create a loan broker backed by this vault
-                auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+                auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
                 env(set(owner, vaultKeylet.key));
                 env.close();
 
@@ -5328,7 +5328,7 @@ class Vault_test : public beast::unit_test::Suite
                 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
 
                 // Create a loan broker backed by this vault
-                auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+                auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
                 env(set(owner, vaultKeylet.key));
                 env.close();
 
@@ -5382,7 +5382,7 @@ class Vault_test : public beast::unit_test::Suite
                     return;
                 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
 
-                auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+                auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
                 env(set(owner, vaultKeylet.key));
                 env.close();
 
@@ -5430,7 +5430,7 @@ class Vault_test : public beast::unit_test::Suite
                     return;
                 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
 
-                auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+                auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
                 env(set(owner, vaultKeylet.key));
                 env.close();
 
@@ -5537,7 +5537,7 @@ class Vault_test : public beast::unit_test::Suite
             PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
 
             // Create a loan broker backed by this vault
-            auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
+            auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
             env(set(owner, vaultKeylet.key));
             env.close();
 
@@ -6338,7 +6338,7 @@ class Vault_test : public beast::unit_test::Suite
         env.close();
 
         // Loan broker: no cover, no management fee, debt cap 10x principal.
-        f.brokerID = keylet::loanbroker(f.lender.id(), env.seq(f.lender)).key;
+        f.brokerID = keylet::loanBroker(f.lender.id(), env.seq(f.lender)).key;
         {
             using namespace loanBroker;
             env(set(f.lender, vaultKeylet.key),
@@ -6347,7 +6347,7 @@ class Vault_test : public beast::unit_test::Suite
         }
 
         // Loan: 3,333 USD principal, impaired immediately.
-        auto const sleBroker = env.le(keylet::loanbroker(f.brokerID));
+        auto const sleBroker = env.le(keylet::loanBroker(f.brokerID));
         if (!BEAST_EXPECT(sleBroker))
             return f;
         f.loanKeylet = keylet::loan(f.brokerID, sleBroker->at(sfLoanSequence));
@@ -6392,7 +6392,7 @@ class Vault_test : public beast::unit_test::Suite
             return f;
         f.sharesLender = tokenLender->getFieldU64(sfMPTAmount);
 
-        auto const sleIssuance = env.le(keylet::mptIssuance(f.shareAsset));
+        auto const sleIssuance = env.le(keylet::mptokenIssuance(f.shareAsset));
         if (!BEAST_EXPECT(sleIssuance))
             return f;
         BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == f.sharesLender);
@@ -6482,7 +6482,7 @@ class Vault_test : public beast::unit_test::Suite
         auto const vaultAfter = env.le(vaultKey);
         if (!BEAST_EXPECT(vaultAfter))
             return;
-        auto const issuanceAfter = env.le(keylet::mptIssuance(f.shareAsset));
+        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
         if (!BEAST_EXPECT(issuanceAfter))
             return;
 
@@ -6580,7 +6580,7 @@ class Vault_test : public beast::unit_test::Suite
         auto const vaultAfter = env.le(vaultKey);
         if (!BEAST_EXPECT(vaultAfter))
             return;
-        auto const issuanceAfter = env.le(keylet::mptIssuance(f.shareAsset));
+        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
         if (!BEAST_EXPECT(issuanceAfter))
             return;
         BEAST_EXPECT(issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender);
@@ -6671,7 +6671,7 @@ class Vault_test : public beast::unit_test::Suite
         auto const vaultFinal = env.le(vaultKey);
         if (!BEAST_EXPECT(vaultFinal))
             return;
-        auto const issuanceFinal = env.le(keylet::mptIssuance(f.shareAsset));
+        auto const issuanceFinal = env.le(keylet::mptokenIssuance(f.shareAsset));
         if (!BEAST_EXPECT(issuanceFinal))
             return;
 
@@ -6753,7 +6753,7 @@ class Vault_test : public beast::unit_test::Suite
         auto const vaultFinal = env.le(vaultKeylet);
         if (!BEAST_EXPECT(vaultFinal))
             return;
-        auto const issuanceFinal = env.le(keylet::mptIssuance(shareAsset));
+        auto const issuanceFinal = env.le(keylet::mptokenIssuance(shareAsset));
         if (!BEAST_EXPECT(issuanceFinal))
             return;
         BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
@@ -6836,7 +6836,7 @@ class Vault_test : public beast::unit_test::Suite
         auto const vaultAfter = env.le(vaultKey);
         if (!BEAST_EXPECT(vaultAfter))
             return;
-        auto const issuanceAfter = env.le(keylet::mptIssuance(f.shareAsset));
+        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
         if (!BEAST_EXPECT(issuanceAfter))
             return;
 
@@ -7355,7 +7355,7 @@ class Vault_test : public beast::unit_test::Suite
             auto const sleVault = env.le(vaultKeylet);
             if (!sleVault)
                 return std::nullopt;
-            auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
             if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
                 return std::nullopt;
             return sleIssuance->getFieldH256(sfReferenceHolding);
@@ -7415,13 +7415,13 @@ class Vault_test : public beast::unit_test::Suite
             auto const sleVault = env.le(keylet);
             BEAST_EXPECT(sleVault != nullptr);
             auto const pseudoId = sleVault->at(sfAccount);
-            auto const expected = keylet::line(pseudoId, asset.raw().get()).key;
+            auto const expected = keylet::trustLine(pseudoId, asset.raw().get()).key;
 
             auto const stored = readReferenceHolding(env, keylet);
             BEAST_EXPECT(stored.has_value());
             BEAST_EXPECT(stored && *stored == expected);
             // The pointed-to RippleState must actually exist.
-            BEAST_EXPECT(env.le(keylet::line(pseudoId, asset.raw().get())) != nullptr);
+            BEAST_EXPECT(env.le(keylet::trustLine(pseudoId, asset.raw().get())) != nullptr);
         }
 
         // XRP-backed vaults leave the field absent: XRP has no separate
@@ -7479,7 +7479,7 @@ class Vault_test : public beast::unit_test::Suite
             mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
             env.close();
 
-            auto const sleIssuance = env.le(keylet::mptIssuance(mptt.issuanceID()));
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(mptt.issuanceID()));
             if (BEAST_EXPECT(sleIssuance))
                 BEAST_EXPECT(!sleIssuance->isFieldPresent(sfReferenceHolding));
         }
@@ -7505,7 +7505,7 @@ class Vault_test : public beast::unit_test::Suite
             auto const sleVault = env.le(vaultKeylet);
             if (!sleVault)
                 return false;
-            auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
             if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
                 return false;
             auto const holdingKey = sleIssuance->getFieldH256(sfReferenceHolding);
@@ -7741,7 +7741,7 @@ class Vault_test : public beast::unit_test::Suite
 
             BEAST_EXPECT(env.le(keylet) == nullptr);
             BEAST_EXPECT(env.le(holdingKeylet) == nullptr);
-            BEAST_EXPECT(env.le(keylet::mptIssuance(sharedMptId)) == nullptr);
+            BEAST_EXPECT(env.le(keylet::mptokenIssuance(sharedMptId)) == nullptr);
         }
     }
 
diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp
index 707e1338a7..db45deb6de 100644
--- a/src/test/jtx/impl/Env.cpp
+++ b/src/test/jtx/impl/Env.cpp
@@ -213,7 +213,7 @@ Env::balance(Account const& account, Asset const& asset) const
         [&](Issue const& issue) -> PrettyAmount {
             if (isXRP(issue.currency))
                 return balance(account);
-            auto const sle = le(keylet::line(account.id(), issue));
+            auto const sle = le(keylet::trustLine(account.id(), issue));
             if (!sle)
                 return {STAmount(issue, 0), account.name()};
             auto amount = sle->getFieldAmount(sfBalance);
@@ -231,7 +231,7 @@ Env::balance(Account const& account, Asset const& asset) const
             if (account.id() == issuer)
             {
                 // Issuer balance
-                auto const sle = le(keylet::mptIssuance(id));
+                auto const sle = le(keylet::mptokenIssuance(id));
                 if (!sle)
                     return {STAmount(mptIssue, 0), account.name()};
 
@@ -253,7 +253,7 @@ Env::balance(Account const& account, Asset const& asset) const
 PrettyAmount
 Env::limit(Account const& account, Issue const& issue) const
 {
-    auto const sle = le(keylet::line(account.id(), issue));
+    auto const sle = le(keylet::trustLine(account.id(), issue));
     if (!sle)
         return {STAmount(issue, 0), account.name()};
     auto const aHigh = account.id() > issue.account;
diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp
index a8ec899c8a..95c8a68436 100644
--- a/src/test/jtx/impl/TestHelpers.cpp
+++ b/src/test/jtx/impl/TestHelpers.cpp
@@ -338,7 +338,7 @@ xrpMinusFee(Env const& env, std::int64_t xrpAmount)
 [[nodiscard]] bool
 expectHolding(Env& env, AccountID const& account, STAmount const& value, bool defaultLimits)
 {
-    if (auto const sle = env.le(keylet::line(account, value.get())))
+    if (auto const sle = env.le(keylet::trustLine(account, value.get())))
     {
         Issue const issue = value.get();
         bool const accountLow = account < issue.account;
@@ -368,7 +368,7 @@ expectHolding(Env& env, AccountID const& account, STAmount const& value, bool de
 [[nodiscard]] bool
 expectHolding(Env& env, AccountID const& account, None const&, Issue const& issue)
 {
-    return !env.le(keylet::line(account, issue));
+    return !env.le(keylet::trustLine(account, issue));
 }
 
 [[nodiscard]] bool
@@ -388,7 +388,7 @@ expectHolding(Env& env, AccountID const& account, None const& value)
 [[nodiscard]] bool
 expectMPT(Env& env, AccountID const& account, STAmount const& value)
 {
-    auto const mptIssuanceID = keylet::mptIssuance(value.asset().get());
+    auto const mptIssuanceID = keylet::mptokenIssuance(value.asset().get());
     auto const mptToken = env.le(keylet::mptoken(mptIssuanceID.key, account));
     return mptToken && (*mptToken)[sfMPTAmount] == value.mpt().value();
 }
@@ -553,7 +553,7 @@ claim(
 uint256
 channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue)
 {
-    auto const k = keylet::payChan(account, dst, seqProxyValue);
+    auto const k = keylet::payChannel(account, dst, seqProxyValue);
     return k.key;
 }
 
diff --git a/src/test/jtx/impl/balance.cpp b/src/test/jtx/impl/balance.cpp
index ddb45a8a97..9d1b812641 100644
--- a/src/test/jtx/impl/balance.cpp
+++ b/src/test/jtx/impl/balance.cpp
@@ -36,7 +36,7 @@ doBalance(Env& env, AccountID const& account, bool kNone, STAmount const& value,
     }
     else
     {
-        auto const sle = env.le(keylet::line(account, issue));
+        auto const sle = env.le(keylet::trustLine(account, issue));
         if (kNone)
         {
             TEST_EXPECT(!sle);
diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp
index 7da3305eec..84c5b5fbec 100644
--- a/src/test/jtx/impl/mpt.cpp
+++ b/src/test/jtx/impl/mpt.cpp
@@ -201,8 +201,8 @@ MPTTester::create(MPTCreate const& arg)
     if (!isTesSuccess(submit(arg, jv)))
     {
         // Verify issuance doesn't exist
-        env_.require(
-            RequireAny([&]() -> bool { return env_.le(keylet::mptIssuance(*id_)) == nullptr; }));
+        env_.require(RequireAny(
+            [&]() -> bool { return env_.le(keylet::mptokenIssuance(*id_)) == nullptr; }));
 
         id_.reset();
     }
@@ -465,7 +465,7 @@ MPTTester::forObject(
 {
     if (!id_)
         Throw("MPT has not been created");
-    auto const key = holder ? keylet::mptoken(*id_, holder->id()) : keylet::mptIssuance(*id_);
+    auto const key = holder ? keylet::mptoken(*id_, holder->id()) : keylet::mptokenIssuance(*id_);
     if (auto const sle = env_.le(key))
         return cb(sle);
     return false;
@@ -630,7 +630,7 @@ MPTTester::getBalance(Account const& account) const
         Throw("MPT has not been created");
     if (account == issuer_)
     {
-        if (auto const sle = env_.le(keylet::mptIssuance(*id_)))
+        if (auto const sle = env_.le(keylet::mptokenIssuance(*id_)))
             return sle->getFieldU64(sfOutstandingAmount);
     }
     else
diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp
index 421f6bd1fa..bd7b07e936 100644
--- a/src/test/rpc/AccountTx_test.cpp
+++ b/src/test/rpc/AccountTx_test.cpp
@@ -589,7 +589,7 @@ class AccountTx_test : public beast::unit_test::Suite
             env(payChanCreate, Sig(alie));
             env.close();
 
-            std::string const payChanIndex{strHex(keylet::payChan(alice, gw, payChanSeq).key)};
+            std::string const payChanIndex{strHex(keylet::payChannel(alice, gw, payChanSeq).key)};
 
             {
                 json::Value payChanFund;
diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp
index dd9eb1c119..14908cf72a 100644
--- a/src/test/rpc/LedgerEntry_test.cpp
+++ b/src/test/rpc/LedgerEntry_test.cpp
@@ -1476,7 +1476,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
 
         // positive test
         {
-            Keylet const keylet = keylet::fees();
+            Keylet const keylet = keylet::feeSettings();
             json::Value jvParams;
             jvParams[jss::fee] = to_string(keylet.key);
             json::Value const jrr =
@@ -1526,7 +1526,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
         uint256 const nftokenID0 = token::getNextID(env, issuer, 0, tfTransferable);
         env(token::mint(issuer, 0), Txflags(tfTransferable));
         env.close();
-        uint256 const offerID = keylet::nftoffer(issuer, env.seq(issuer)).key;
+        uint256 const offerID = keylet::nftokenOffer(issuer, env.seq(issuer)).key;
         env(token::createOffer(issuer, nftokenID0, drops(1)),
             token::Destination(buyer),
             Txflags(tfSellNFToken));
@@ -1560,7 +1560,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
         env(token::mint(issuer, 0), Txflags(tfTransferable));
         env.close();
 
-        auto const nftpage = keylet::nftpageMax(issuer);
+        auto const nftpage = keylet::nftokenPageMax(issuer);
         BEAST_EXPECT(env.le(nftpage) != nullptr);
 
         {
@@ -1710,7 +1710,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
 
         std::string const ledgerHash{to_string(env.closed()->header().hash)};
 
-        uint256 const payChanIndex{keylet::payChan(alice, env.master, env.seq(alice) - 1).key};
+        uint256 const payChanIndex{keylet::payChannel(alice, env.master, env.seq(alice) - 1).key};
         {
             // Request the payment channel using its index.
             json::Value jvParams;
@@ -2421,7 +2421,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
         };
 
         test(jss::amendments, jss::Amendments, keylet::amendments(), true);
-        test(jss::fee, jss::FeeSettings, keylet::fees(), true);
+        test(jss::fee, jss::FeeSettings, keylet::feeSettings(), true);
         // There won't be an nunl
         test(jss::nunl, jss::NegativeUNL, keylet::negativeUNL(), false);
         // Can only get the short skip list this way
diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp
index 5c519f3869..97c5290947 100644
--- a/src/test/rpc/Subscribe_test.cpp
+++ b/src/test/rpc/Subscribe_test.cpp
@@ -1452,12 +1452,12 @@ public:
             // Alice creates one sell offer for each NFT
             // Verify the offer indexes are correct in the NFTokenCreateOffer tx
             // meta
-            uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId1, drops(1)), Txflags(tfSellNFToken));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(aliceOfferIndex1);
 
-            uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId2, drops(1)), Txflags(tfSellNFToken));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(aliceOfferIndex2);
@@ -1471,7 +1471,7 @@ public:
 
             // Bobs creates a buy offer for nftId1
             // Verify the offer id is correct in the NFTokenCreateOffer tx meta
-            auto const bobBuyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
+            auto const bobBuyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key;
             env(token::createOffer(bob, nftId1, drops(1)), token::Owner(alice));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(bobBuyOfferIndex);
@@ -1492,7 +1492,7 @@ public:
             verifyNFTokenID(nftId);
 
             // Alice creates sell offer and set broker as destination
-            uint256 const offerAliceToBroker = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const offerAliceToBroker = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, drops(1)),
                 token::Destination(broker),
                 Txflags(tfSellNFToken));
@@ -1500,7 +1500,7 @@ public:
             verifyNFTokenOfferID(offerAliceToBroker);
 
             // Bob creates buy offer
-            uint256 const offerBobToBroker = keylet::nftoffer(bob, env.seq(bob)).key;
+            uint256 const offerBobToBroker = keylet::nftokenOffer(bob, env.seq(bob)).key;
             env(token::createOffer(bob, nftId, drops(1)), token::Owner(alice));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(offerBobToBroker);
@@ -1521,12 +1521,12 @@ public:
             verifyNFTokenID(nftId);
 
             // Alice creates 2 sell offers for the same NFT
-            uint256 const aliceOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(aliceOfferIndex1);
 
-            uint256 const aliceOfferIndex2 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(aliceOfferIndex2);
@@ -1540,7 +1540,8 @@ public:
 
         if (features[featureNFTokenMintOffer])
         {
-            uint256 const aliceMintWithOfferIndex1 = keylet::nftoffer(alice, env.seq(alice)).key;
+            uint256 const aliceMintWithOfferIndex1 =
+                keylet::nftokenOffer(alice, env.seq(alice)).key;
             env(token::mint(alice), token::Amount(XRP(0)));
             BEAST_EXPECT(env.syncClose());
             verifyNFTokenOfferID(aliceMintWithOfferIndex1);
diff --git a/src/tests/libxrpl/helpers/TxTest.cpp b/src/tests/libxrpl/helpers/TxTest.cpp
index aeaa805b27..86e36b8abf 100644
--- a/src/tests/libxrpl/helpers/TxTest.cpp
+++ b/src/tests/libxrpl/helpers/TxTest.cpp
@@ -231,13 +231,13 @@ TxTest::getCloseTime() const
 STAmount
 TxTest::getBalance(AccountID const& account, IOU const& iou) const
 {
-    auto const sle = openLedger_->read(keylet::line(account, iou.issue()));
+    auto const sle = openLedger_->read(keylet::trustLine(account, iou.issue()));
     if (!sle)
         return STAmount{iou.issue(), 0};
 
-    auto const rippleState = ledger_entries::RippleState{sle};
+    auto const trustLine = ledger_entries::RippleState{sle};
 
-    auto balance = rippleState.getBalance();
+    auto balance = trustLine.getBalance();
     if (iou.issue().account == account)
     {
         throw std::logic_error("TxTest::getBalance: account is issuer");
diff --git a/src/tests/libxrpl/tx/AccountSet.cpp b/src/tests/libxrpl/tx/AccountSet.cpp
index 726e6e9024..46b298dde8 100644
--- a/src/tests/libxrpl/tx/AccountSet.cpp
+++ b/src/tests/libxrpl/tx/AccountSet.cpp
@@ -619,7 +619,7 @@ TEST(AccountSet, Ticket)
     // Verify alice has 1 owner object (the ticket)
     EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 1u);
     // Verify ticket exists
-    EXPECT_TRUE(env.getClosedLedger().exists(keylet::kTicket(alice.id(), ticketSeq)));
+    EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
 
     // Try using a ticket that alice doesn't have
     EXPECT_EQ(
@@ -629,7 +629,7 @@ TEST(AccountSet, Ticket)
     env.close();
 
     // Verify ticket still exists
-    EXPECT_TRUE(env.getClosedLedger().exists(keylet::kTicket(alice.id(), ticketSeq)));
+    EXPECT_TRUE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
 
     // Get alice's sequence before using the ticket
     std::uint32_t const aliceSeq = env.getAccountRoot(alice.id()).getSequence();
@@ -642,7 +642,7 @@ TEST(AccountSet, Ticket)
 
     // Verify ticket is consumed (no owner objects)
     EXPECT_EQ(env.getAccountRoot(alice.id()).getOwnerCount(), 0u);
-    EXPECT_FALSE(env.getClosedLedger().exists(keylet::kTicket(alice.id(), ticketSeq)));
+    EXPECT_FALSE(env.getClosedLedger().exists(keylet::ticket(alice.id(), ticketSeq)));
 
     // Verify alice's sequence did NOT advance (ticket use doesn't increment seq)
     EXPECT_EQ(env.getAccountRoot(alice.id()).getSequence(), aliceSeq);
diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp
index 2a4121ede9..2eb64ecaf9 100644
--- a/src/xrpld/app/ledger/detail/BuildLedger.cpp
+++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp
@@ -73,7 +73,7 @@ buildLedgerImpl(
 
     // Accept ledger
     XRPL_ASSERT(
-        built->header().seq < kXrpLedgerEarliestFees || built->read(keylet::fees()),
+        built->header().seq < kXrpLedgerEarliestFees || built->read(keylet::feeSettings()),
         "xrpl::buildLedgerImpl : valid ledger fees");
     built->setAccepted(closeTime, closeResolution, closeTimeCorrect);
 
diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp
index 5a9f24cc2e..e4df126ee8 100644
--- a/src/xrpld/app/ledger/detail/InboundLedger.cpp
+++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp
@@ -109,7 +109,7 @@ InboundLedger::init(ScopedLockType& collectionLock)
     JLOG(journal_.debug()) << "Acquiring ledger we already have in "
                            << " local store. " << hash_;
     XRPL_ASSERT(
-        ledger_->header().seq < kXrpLedgerEarliestFees || ledger_->read(keylet::fees()),
+        ledger_->header().seq < kXrpLedgerEarliestFees || ledger_->read(keylet::feeSettings()),
         "xrpl::InboundLedger::init : valid ledger fees");
     ledger_->setImmutable();
 
@@ -331,7 +331,7 @@ InboundLedger::tryDB(NodeStore::Database& srcDB)
         JLOG(journal_.debug()) << "Had everything locally";
         complete_ = true;
         XRPL_ASSERT(
-            ledger_->header().seq < kXrpLedgerEarliestFees || ledger_->read(keylet::fees()),
+            ledger_->header().seq < kXrpLedgerEarliestFees || ledger_->read(keylet::feeSettings()),
             "xrpl::InboundLedger::tryDB : valid ledger fees");
         ledger_->setImmutable();
     }
@@ -427,7 +427,7 @@ InboundLedger::done()
     if (complete_ && !failed_ && ledger_)
     {
         XRPL_ASSERT(
-            ledger_->header().seq < kXrpLedgerEarliestFees || ledger_->read(keylet::fees()),
+            ledger_->header().seq < kXrpLedgerEarliestFees || ledger_->read(keylet::feeSettings()),
             "xrpl::InboundLedger::done : valid ledger fees");
         ledger_->setImmutable();
         switch (reason_)
diff --git a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp
index 579f66063c..3561b66951 100644
--- a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp
@@ -122,7 +122,7 @@ finishLoadByIndexOrHash(std::shared_ptr const& ledger, beast::Journal j)
         return;
 
     XRPL_ASSERT(
-        ledger->header().seq < kXrpLedgerEarliestFees || ledger->read(keylet::fees()),
+        ledger->header().seq < kXrpLedgerEarliestFees || ledger->read(keylet::feeSettings()),
         "xrpl::finishLoadByIndexOrHash : valid ledger fees");
     ledger->setImmutable();
 
diff --git a/src/xrpld/app/ledger/detail/LocalTxs.cpp b/src/xrpld/app/ledger/detail/LocalTxs.cpp
index d843acfe44..5bfe8684f0 100644
--- a/src/xrpld/app/ledger/detail/LocalTxs.cpp
+++ b/src/xrpld/app/ledger/detail/LocalTxs.cpp
@@ -163,7 +163,7 @@ public:
 
             // Ticket should have been created by now.  Remove if ticket
             // does not exist.
-            return !view.exists(keylet::kTicket(acctID, seqProx));
+            return !view.exists(keylet::ticket(acctID, seqProx));
         });
     }
 
diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp
index c2fb78ce79..08d84636f6 100644
--- a/src/xrpld/app/main/Application.cpp
+++ b/src/xrpld/app/main/Application.cpp
@@ -1681,7 +1681,7 @@ ApplicationImp::startGenesisLedger()
     auto const next = std::make_shared(*genesis, getTimeKeeper().closeTime());
     next->updateSkipList();
     XRPL_ASSERT(
-        next->header().seq < kXrpLedgerEarliestFees || next->read(keylet::fees()),
+        next->header().seq < kXrpLedgerEarliestFees || next->read(keylet::feeSettings()),
         "xrpl::ApplicationImp::startGenesisLedger : valid ledger fees");
     next->setImmutable();
     openLedger_.emplace(next, cachedSLEs_, logs_->journal("OpenLedger"));
@@ -1703,7 +1703,7 @@ ApplicationImp::getLastFullLedger()
             return ledger;
 
         XRPL_ASSERT(
-            ledger->header().seq < kXrpLedgerEarliestFees || ledger->read(keylet::fees()),
+            ledger->header().seq < kXrpLedgerEarliestFees || ledger->read(keylet::feeSettings()),
             "xrpl::ApplicationImp::getLastFullLedger : valid ledger fees");
         ledger->setImmutable();
 
@@ -1854,7 +1854,8 @@ ApplicationImp::loadLedgerFromFile(std::string const& name)
         loadLedger->stateMap().flushDirty(NodeObjectType::AccountNode);
 
         XRPL_ASSERT(
-            loadLedger->header().seq < kXrpLedgerEarliestFees || loadLedger->read(keylet::fees()),
+            loadLedger->header().seq < kXrpLedgerEarliestFees ||
+                loadLedger->read(keylet::feeSettings()),
             "xrpl::ApplicationImp::loadLedgerFromFile : valid ledger fees");
         loadLedger->setAccepted(closeTime, closeTimeResolution, !closeTimeEstimated);
 
diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp
index 0326828a70..9022095ea8 100644
--- a/src/xrpld/app/misc/detail/TxQ.cpp
+++ b/src/xrpld/app/misc/detail/TxQ.cpp
@@ -760,7 +760,7 @@ TxQ::apply(
     // If the transaction needs a Ticket is that Ticket in the ledger?
     SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]);
     SeqProxy const txSeqProx = tx->getSeqProxy();
-    if (txSeqProx.isTicket() && !view.exists(keylet::kTicket(account, txSeqProx)))
+    if (txSeqProx.isTicket() && !view.exists(keylet::ticket(account, txSeqProx)))
     {
         if (txSeqProx.value() < acctSeqProx.value())
         {
diff --git a/src/xrpld/rpc/detail/AssetCache.cpp b/src/xrpld/rpc/detail/AssetCache.cpp
index 23cc31252d..e29f5659f9 100644
--- a/src/xrpld/rpc/detail/AssetCache.cpp
+++ b/src/xrpld/rpc/detail/AssetCache.cpp
@@ -136,7 +136,7 @@ AssetCache::getMPTs(xrpl::AccountID const& account)
             auto const mptID = sle->getFieldH192(sfMPTokenIssuanceID);
             bool const zeroBalance = sle->at(sfMPTAmount) == 0;
             bool const maxedOut = [&] {
-                if (auto const sleIssuance = ledger_->read(keylet::mptIssuance(mptID)))
+                if (auto const sleIssuance = ledger_->read(keylet::mptokenIssuance(mptID)))
                 {
                     return sleIssuance->at(sfOutstandingAmount) == maxMPTAmount(*sleIssuance);
                 }
diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp
index e1a2a4acf6..d7566622e8 100644
--- a/src/xrpld/rpc/detail/Pathfinder.cpp
+++ b/src/xrpld/rpc/detail/Pathfinder.cpp
@@ -951,7 +951,7 @@ Pathfinder::isNoRipple(
     AccountID const& toAccount,
     Currency const& currency)
 {
-    auto sleRipple = ledger_->read(keylet::line(toAccount, fromAccount, currency));
+    auto sleRipple = ledger_->read(keylet::trustLine(toAccount, fromAccount, currency));
 
     auto const flag((toAccount > fromAccount) ? lsfHighNoRipple : lsfLowNoRipple);
 
diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp
index 7ab2468b75..1764375812 100644
--- a/src/xrpld/rpc/detail/RPCHelpers.cpp
+++ b/src/xrpld/rpc/detail/RPCHelpers.cpp
@@ -86,7 +86,7 @@ isRelatedToAccount(ReadView const& ledger, SLE::const_ref sle, AccountID const&
     }
     if (sle->getType() == ltSIGNER_LIST)
     {
-        Keylet const accountSignerList = keylet::signers(accountID);
+        Keylet const accountSignerList = keylet::signerList(accountID);
         return sle->key() == accountSignerList.key;
     }
     if (sle->getType() == ltNFTOKEN_OFFER)
diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp
index 2c00205131..b6d2fe259f 100644
--- a/src/xrpld/rpc/handlers/VaultInfo.cpp
+++ b/src/xrpld/rpc/handlers/VaultInfo.cpp
@@ -79,7 +79,7 @@ doVaultInfo(RPC::JsonContext& context)
     auto const sleVault = lpLedger->read(keylet::vault(uNodeIndex));
     auto const sleIssuance = sleVault == nullptr  //
         ? nullptr
-        : lpLedger->read(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
+        : lpLedger->read(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
     if (!sleVault || !sleIssuance)
     {
         jvResult[jss::error] = "entryNotFound";
diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp
index d0f45cbc6f..fba48ea93e 100644
--- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp
@@ -222,7 +222,7 @@ doAccountInfo(RPC::JsonContext& context)
 
             // This code will need to be revisited if in the future we support
             // multiple SignerLists on one account.
-            auto const sleSigners = ledger->read(keylet::signers(accountID));
+            auto const sleSigners = ledger->read(keylet::signerList(accountID));
             if (sleSigners)
                 jvSignerList.append(sleSigners->getJson(JsonOptions::Values::None));
 
diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp
index 0eb91206f3..0249963908 100644
--- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp
@@ -74,8 +74,8 @@ doAccountNFTs(RPC::JsonContext& context)
             return RPC::invalidFieldError(jss::marker);
     }
 
-    auto const first = keylet::nftpage(keylet::nftpageMin(accountID), marker);
-    auto const last = keylet::nftpageMax(accountID);
+    auto const first = keylet::nftokenPage(keylet::nftokenPageMin(accountID), marker);
+    auto const last = keylet::nftokenPageMax(accountID);
 
     auto cp = ledger->read(
         Keylet(ltNFTOKEN_PAGE, ledger->succ(first.key, last.key.next()).value_or(last.key)));
@@ -134,7 +134,7 @@ doAccountNFTs(RPC::JsonContext& context)
                 obj[sfFlags.jsonName] = nft::getFlags(nftokenID);
                 obj[sfIssuer.jsonName] = to_string(nft::getIssuer(nftokenID));
                 obj[sfNFTokenTaxon.jsonName] = nft::toUInt32(nft::getTaxon(nftokenID));
-                obj[jss::nft_serial] = nft::getSerial(nftokenID);
+                obj[jss::nft_serial] = nft::getSequence(nftokenID);
                 if (std::uint16_t const xferFee = {nft::getTransferFee(nftokenID)})
                     obj[sfTransferFee.jsonName] = xferFee;
             }
diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp
index 08a7fbe44a..8375feb4e7 100644
--- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp
@@ -61,7 +61,7 @@ getAccountObjects(
         (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), ltNFTOKEN_PAGE)) &&
         dirIndex.isZero();
 
-    Keylet const firstNFTPage = keylet::nftpageMin(account);
+    Keylet const firstNFTPage = keylet::nftokenPageMin(account);
 
     // we need to check the marker to see if it is an NFTTokenPage index.
     if (iterateNFTPages && entryIndex.isNonZero())
@@ -85,7 +85,7 @@ getAccountObjects(
         Keylet const first =
             entryIndex.isZero() ? firstNFTPage : Keylet{ltNFTOKEN_PAGE, entryIndex};
 
-        Keylet const last = keylet::nftpageMax(account);
+        Keylet const last = keylet::nftokenPageMax(account);
 
         auto currentKey = ledger.succ(first.key, last.key.next()).value_or(last.key);
 
diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp
index 236712f0c2..2dd7ae34d4 100644
--- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp
+++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp
@@ -82,7 +82,7 @@ parseIndex(json::Value const& params, json::StaticString const fieldName, unsign
         if (index == jss::amendments.cStr())
             return keylet::amendments().key;
         if (index == jss::fee.cStr())
-            return keylet::fees().key;
+            return keylet::feeSettings().key;
         if (index == jss::nunl)
             return keylet::negativeUNL().key;
         if (index == jss::hashes)
@@ -434,7 +434,7 @@ parseEscrow(
     return keylet::escrow(*id, *seq).key;
 }
 
-auto const parseFeeSettings = fixed(keylet::fees());
+auto const parseFeeSettings = fixed(keylet::feeSettings());
 
 static std::expected
 parseFixed(
@@ -493,7 +493,7 @@ parseLoanBroker(
     if (!seq)
         return std::unexpected(seq.error());
 
-    return keylet::loanbroker(*id, *seq).key;
+    return keylet::loanBroker(*id, *seq).key;
 }
 
 static std::expected
@@ -555,7 +555,7 @@ parseMPTokenIssuance(
             "malformedMPTokenIssuance", fieldName, "Hash192");
     }
 
-    return keylet::mptIssuance(*mptIssuanceID).key;
+    return keylet::mptokenIssuance(*mptIssuanceID).key;
 }
 
 static std::expected
@@ -707,7 +707,7 @@ parseRippleState(
             "malformedCurrency", jss::currency, "Currency");
     }
 
-    return keylet::line(*id1, *id2, uCurrency).key;
+    return keylet::trustLine(*id1, *id2, uCurrency).key;
 }
 
 static std::expected
diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
index 8529ec2d2c..42b99a0009 100644
--- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
+++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
@@ -78,7 +78,7 @@ enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const
         if (!startAfter.parseHex(marker.asString()))
             return rpcError(RpcInvalidParams);
 
-        auto const sle = ledger->read(keylet::nftoffer(startAfter));
+        auto const sle = ledger->read(keylet::nftokenOffer(startAfter));
 
         if (!sle || nftId != sle->getFieldH256(sfNFTokenID))
             return rpcError(RpcInvalidParams);

From 12a5d9014ebf2e45cc763688c6d7a58fa4139c82 Mon Sep 17 00:00:00 2001
From: Timothy Banks 
Date: Fri, 26 Jun 2026 06:24:25 -0400
Subject: [PATCH 014/100] refactor: Retire Clawback amendment (#7353)

---
 include/xrpl/protocol/detail/features.macro   |  2 +-
 .../xrpl/protocol/detail/transactions.macro   |  2 +-
 .../protocol_autogen/transactions/Clawback.h  |  2 +-
 src/libxrpl/ledger/CanonicalTXSet.cpp         |  3 +-
 .../tx/transactors/account/AccountSet.cpp     | 39 +++++------
 src/test/app/Clawback_test.cpp                | 69 -------------------
 src/test/app/Delegate_test.cpp                |  1 -
 src/test/rpc/AccountInfo_test.cpp             | 30 +++-----
 .../rpc/handlers/account/AccountInfo.cpp      |  7 +-
 9 files changed, 36 insertions(+), 119 deletions(-)

diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index 573dca951d..1ccb60d3af 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -64,7 +64,6 @@ XRPL_FEATURE(DID,                        Supported::Yes, VoteBehavior::DefaultNo
 XRPL_FIX    (DisallowIncomingV1,         Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(XChainBridge,               Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(AMM,                        Supported::Yes, VoteBehavior::DefaultNo)
-XRPL_FEATURE(Clawback,                   Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(XRPFees,                    Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (RemoveNFTokenAutoTrustLine, Supported::Yes, VoteBehavior::DefaultYes)
 
@@ -116,6 +115,7 @@ XRPL_RETIRE_FIX(UniversalNumber)
 
 XRPL_RETIRE_FEATURE(Checks)
 XRPL_RETIRE_FEATURE(CheckCashMakesTrustLine)
+XRPL_RETIRE_FEATURE(Clawback)
 XRPL_RETIRE_FEATURE(CryptoConditions)
 XRPL_RETIRE_FEATURE(CryptoConditionsSuite)
 XRPL_RETIRE_FEATURE(DeletableAccounts)
diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro
index 450e2558cc..dbaaf46083 100644
--- a/include/xrpl/protocol/detail/transactions.macro
+++ b/include/xrpl/protocol/detail/transactions.macro
@@ -395,7 +395,7 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer,
 #endif
 TRANSACTION(ttCLAWBACK, 30, Clawback,
     Delegation::Delegable,
-    featureClawback,
+    uint256{},
     NoPriv,
     ({
     {sfAmount, SoeRequired, SoeMptSupported},
diff --git a/include/xrpl/protocol_autogen/transactions/Clawback.h b/include/xrpl/protocol_autogen/transactions/Clawback.h
index bbf1c411f3..ecd7ebe7a2 100644
--- a/include/xrpl/protocol_autogen/transactions/Clawback.h
+++ b/include/xrpl/protocol_autogen/transactions/Clawback.h
@@ -20,7 +20,7 @@ class ClawbackBuilder;
  *
  * Type: ttCLAWBACK (30)
  * Delegable: Delegation::Delegable
- * Amendment: featureClawback
+ * Amendment: uint256{}
  * Privileges: NoPriv
  *
  * Immutable wrapper around STTx providing type-safe field access.
diff --git a/src/libxrpl/ledger/CanonicalTXSet.cpp b/src/libxrpl/ledger/CanonicalTXSet.cpp
index df4e88e346..12cec5e321 100644
--- a/src/libxrpl/ledger/CanonicalTXSet.cpp
+++ b/src/libxrpl/ledger/CanonicalTXSet.cpp
@@ -42,7 +42,8 @@ CanonicalTXSet::accountKey(AccountID const& account)
 void
 CanonicalTXSet::insert(std::shared_ptr txn)
 {
-    Key key(accountKey(txn->getAccountID(sfAccount)), txn->getSeqProxy(), txn->getTransactionID());
+    Key const key(
+        accountKey(txn->getAccountID(sfAccount)), txn->getSeqProxy(), txn->getTransactionID());
     map_.emplace(key, std::move(txn));
 }
 
diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp b/src/libxrpl/tx/transactors/account/AccountSet.cpp
index dfe7ec5b5f..f0ad5b113a 100644
--- a/src/libxrpl/tx/transactors/account/AccountSet.cpp
+++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp
@@ -194,30 +194,27 @@ AccountSet::preclaim(PreclaimContext const& ctx)
     //
     // Clawback
     //
-    if (ctx.view.rules().enabled(featureClawback))
+    if (uSetFlag == asfAllowTrustLineClawback)
     {
-        if (uSetFlag == asfAllowTrustLineClawback)
+        if (sle->isFlag(lsfNoFreeze))
         {
-            if (sle->isFlag(lsfNoFreeze))
-            {
-                JLOG(ctx.j.trace()) << "Can't set Clawback if NoFreeze is set";
-                return tecNO_PERMISSION;
-            }
-
-            if (!dirIsEmpty(ctx.view, keylet::ownerDir(id)))
-            {
-                JLOG(ctx.j.trace()) << "Owner directory not empty.";
-                return tecOWNERS;
-            }
+            JLOG(ctx.j.trace()) << "Can't set Clawback if NoFreeze is set";
+            return tecNO_PERMISSION;
         }
-        else if (uSetFlag == asfNoFreeze)
+
+        if (!dirIsEmpty(ctx.view, keylet::ownerDir(id)))
         {
-            // Cannot set NoFreeze if clawback is enabled
-            if (sle->isFlag(lsfAllowTrustLineClawback))
-            {
-                JLOG(ctx.j.trace()) << "Can't set NoFreeze if clawback is enabled";
-                return tecNO_PERMISSION;
-            }
+            JLOG(ctx.j.trace()) << "Owner directory not empty.";
+            return tecOWNERS;
+        }
+    }
+    else if (uSetFlag == asfNoFreeze)
+    {
+        // Cannot set NoFreeze if clawback is enabled
+        if (sle->isFlag(lsfAllowTrustLineClawback))
+        {
+            JLOG(ctx.j.trace()) << "Can't set NoFreeze if clawback is enabled";
+            return tecNO_PERMISSION;
         }
     }
 
@@ -576,7 +573,7 @@ AccountSet::doApply()
     }
 
     // Set flag for clawback
-    if (ctx_.view().rules().enabled(featureClawback) && uSetFlag == asfAllowTrustLineClawback)
+    if (uSetFlag == asfAllowTrustLineClawback)
     {
         JLOG(j_.trace()) << "set allow clawback";
         uFlagsOut |= lsfAllowTrustLineClawback;
diff --git a/src/test/app/Clawback_test.cpp b/src/test/app/Clawback_test.cpp
index b1a756382d..2dadd9f503 100644
--- a/src/test/app/Clawback_test.cpp
+++ b/src/test/app/Clawback_test.cpp
@@ -161,35 +161,6 @@ class Clawback_test : public beast::unit_test::Suite
             BEAST_EXPECT(ownerCount(env, alice) == 0);
             BEAST_EXPECT(ownerCount(env, bob) == 0);
         }
-
-        // Test that one cannot enable asfAllowTrustLineClawback when
-        // featureClawback amendment is disabled
-        {
-            Env env(*this, features - featureClawback);
-
-            Account const alice{"alice"};
-
-            env.fund(XRP(1000), alice);
-            env.close();
-
-            env.require(Nflags(alice, asfAllowTrustLineClawback));
-
-            // alice attempts to set asfAllowTrustLineClawback flag while
-            // amendment is disabled. no error is returned, but the flag remains
-            // to be unset.
-            env(fset(alice, asfAllowTrustLineClawback));
-            env.close();
-            env.require(Nflags(alice, asfAllowTrustLineClawback));
-
-            // now enable clawback amendment
-            env.enableFeature(featureClawback);
-            env.close();
-
-            // asfAllowTrustLineClawback can be set
-            env(fset(alice, asfAllowTrustLineClawback));
-            env.close();
-            env.require(Flags(alice, asfAllowTrustLineClawback));
-        }
     }
 
     void
@@ -198,46 +169,6 @@ class Clawback_test : public beast::unit_test::Suite
         testcase("Validation");
         using namespace test::jtx;
 
-        // Test that Clawback tx fails for the following:
-        // 1. when amendment is disabled
-        // 2. when asfAllowTrustLineClawback flag has not been set
-        {
-            Env env(*this, features - featureClawback);
-
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(1000), alice, bob);
-            env.close();
-
-            env.require(Nflags(alice, asfAllowTrustLineClawback));
-
-            auto const usd = alice["USD"];
-
-            // alice issues 10 USD to bob
-            env.trust(usd(1000), bob);
-            env(pay(alice, bob, usd(10)));
-            env.close();
-
-            env.require(Balance(bob, alice["USD"](10)));
-            env.require(Balance(alice, bob["USD"](-10)));
-
-            // clawback fails because amendment is disabled
-            env(claw(alice, bob["USD"](5)), Ter(temDISABLED));
-            env.close();
-
-            // now enable clawback amendment
-            env.enableFeature(featureClawback);
-            env.close();
-
-            // clawback fails because asfAllowTrustLineClawback has not been set
-            env(claw(alice, bob["USD"](5)), Ter(tecNO_PERMISSION));
-            env.close();
-
-            env.require(Balance(bob, alice["USD"](10)));
-            env.require(Balance(alice, bob["USD"](-10)));
-        }
-
         // Test that Clawback tx fails for the following:
         // 1. invalid flag
         // 2. negative STAmount
diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp
index 1516219e46..6e80577797 100644
--- a/src/test/app/Delegate_test.cpp
+++ b/src/test/app/Delegate_test.cpp
@@ -2340,7 +2340,6 @@ class Delegate_test : public beast::unit_test::Suite
         // NFTokenMint, NFTokenBurn, NFTokenCreateOffer, NFTokenCancelOffer,
         // NFTokenAcceptOffer are not included, they are tested separately.
         std::unordered_map txRequiredFeatures{
-            {"Clawback", featureClawback},
             {"AMMClawback", featureAMMClawback},
             {"AMMCreate", featureAMM},
             {"AMMDeposit", featureAMM},
diff --git a/src/test/rpc/AccountInfo_test.cpp b/src/test/rpc/AccountInfo_test.cpp
index 385fc2f58a..d061be1f0e 100644
--- a/src/test/rpc/AccountInfo_test.cpp
+++ b/src/test/rpc/AccountInfo_test.cpp
@@ -583,24 +583,17 @@ public:
         static constexpr std::pair kAllowTrustLineClawbackFlag{
             "allowTrustLineClawback", asfAllowTrustLineClawback};
 
-        if (features[featureClawback])
-        {
-            // must use bob's account because alice has noFreeze set
-            auto const f1 = getAccountFlag(kAllowTrustLineClawbackFlag.first, bob);
-            BEAST_EXPECT(f1.has_value());
-            BEAST_EXPECT(!f1.value());  // NOLINT(bugprone-unchecked-optional-access)
+        // must use bob's account because alice has noFreeze set
+        auto const f1 = getAccountFlag(kAllowTrustLineClawbackFlag.first, bob);
+        BEAST_EXPECT(f1.has_value());
+        BEAST_EXPECT(!f1.value());  // NOLINT(bugprone-unchecked-optional-access)
 
-            // Set allowTrustLineClawback
-            env(fset(bob, kAllowTrustLineClawbackFlag.second));
-            env.close();
-            auto const f2 = getAccountFlag(kAllowTrustLineClawbackFlag.first, bob);
-            BEAST_EXPECT(f2.has_value());
-            BEAST_EXPECT(f2.value());  // NOLINT(bugprone-unchecked-optional-access)
-        }
-        else
-        {
-            BEAST_EXPECT(!getAccountFlag(kAllowTrustLineClawbackFlag.first, bob));
-        }
+        // Set allowTrustLineClawback
+        env(fset(bob, kAllowTrustLineClawbackFlag.second));
+        env.close();
+        auto const f2 = getAccountFlag(kAllowTrustLineClawbackFlag.first, bob);
+        BEAST_EXPECT(f2.has_value());
+        BEAST_EXPECT(f2.value());  // NOLINT(bugprone-unchecked-optional-access)
 
         static constexpr std::pair kAllowTrustLineLockingFlag{
             "allowTrustLineLocking", asfAllowTrustLineLocking};
@@ -634,8 +627,7 @@ public:
 
         FeatureBitset const allFeatures{xrpl::test::jtx::testableAmendments()};
         testAccountFlags(allFeatures);
-        testAccountFlags(allFeatures - featureClawback);
-        testAccountFlags(allFeatures - featureClawback - featureTokenEscrow);
+        testAccountFlags(allFeatures - featureTokenEscrow);
     }
 };
 
diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp
index fba48ea93e..3a96593452 100644
--- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp
@@ -169,11 +169,8 @@ doAccountInfo(RPC::JsonContext& context)
         for (auto const& lsf : kDisallowIncomingFlags)
             acctFlags[lsf.first.data()] = sleAccepted->isFlag(lsf.second);
 
-        if (ledger->rules().enabled(featureClawback))
-        {
-            acctFlags[kAllowTrustLineClawbackFlag.first.data()] =
-                sleAccepted->isFlag(kAllowTrustLineClawbackFlag.second);
-        }
+        acctFlags[kAllowTrustLineClawbackFlag.first.data()] =
+            sleAccepted->isFlag(kAllowTrustLineClawbackFlag.second);
 
         if (ledger->rules().enabled(featureTokenEscrow))
         {

From 2ab43b6fda1dac284d799e7a4755b2279b6c902d Mon Sep 17 00:00:00 2001
From: Timothy Banks 
Date: Fri, 26 Jun 2026 06:31:16 -0400
Subject: [PATCH 015/100] refactor: Retire NFTokenReserve fix (#7367)

---
 include/xrpl/protocol/detail/features.macro   |   2 +-
 .../tx/transactors/nft/NFTokenAcceptOffer.cpp |  30 ++-
 src/test/app/NFToken_test.cpp                 | 189 +++++++-----------
 3 files changed, 86 insertions(+), 135 deletions(-)

diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index 1ccb60d3af..2b6beaa671 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -58,7 +58,6 @@ XRPL_FIX    (EmptyDID,                   Supported::Yes, VoteBehavior::DefaultNo
 XRPL_FEATURE(PriceOracle,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (AMMOverflowOffer,           Supported::Yes, VoteBehavior::DefaultYes)
 XRPL_FIX    (InnerObjTemplate,           Supported::Yes, VoteBehavior::DefaultNo)
-XRPL_FIX    (NFTokenReserve,             Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (FillOrKill,                 Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(DID,                        Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (DisallowIncomingV1,         Supported::Yes, VoteBehavior::DefaultNo)
@@ -104,6 +103,7 @@ XRPL_RETIRE_FIX(CheckThreading)
 XRPL_RETIRE_FIX(MasterKeyAsRegularKey)
 XRPL_RETIRE_FIX(NonFungibleTokensV1_2)
 XRPL_RETIRE_FIX(NFTokenRemint)
+XRPL_RETIRE_FIX(NFTokenReserve)
 XRPL_RETIRE_FIX(PayChanRecipientOwnerDir)
 XRPL_RETIRE_FIX(QualityUpperBound)
 XRPL_RETIRE_FIX(ReducedOffersV1)
diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
index 1d303b7141..14bf3646c0 100644
--- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
+++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
@@ -374,28 +374,22 @@ NFTokenAcceptOffer::transferNFToken(
 
     auto const insertRet = nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
 
-    // if fixNFTokenReserve is enabled, check if the buyer has sufficient
-    // reserve to own a new object, if their OwnerCount changed.
-    //
     // There was an issue where the buyer accepts a sell offer, the ledger
     // didn't check if the buyer has enough reserve, meaning that buyer can get
     // NFTs free of reserve.
-    if (view().rules().enabled(fixNFTokenReserve))
-    {
-        // To check if there is sufficient reserve, we cannot use preFeeBalance_
-        // because NFT is sold for a price. So we must use the balance after
-        // the deduction of the potential offer price. A small caveat here is
-        // that the balance has already deducted the transaction fee, meaning
-        // that the reserve requirement is a few drops higher.
-        auto const buyerBalance = sleBuyer->getFieldAmount(sfBalance);
+    // To check if there is sufficient reserve, we cannot use preFeeBalance_
+    // because NFT is sold for a price. So we must use the balance after
+    // the deduction of the potential offer price. A small caveat here is
+    // that the balance has already deducted the transaction fee, meaning
+    // that the reserve requirement is a few drops higher.
+    auto const buyerBalance = sleBuyer->getFieldAmount(sfBalance);
 
-        auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount);
-        if (buyerOwnerCountAfter > buyerOwnerCountBefore)
-        {
-            if (auto const reserve = view().fees().accountReserve(buyerOwnerCountAfter);
-                buyerBalance < reserve)
-                return tecINSUFFICIENT_RESERVE;
-        }
+    auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount);
+    if (buyerOwnerCountAfter > buyerOwnerCountBefore)
+    {
+        if (auto const reserve = view().fees().accountReserve(buyerOwnerCountAfter);
+            buyerBalance < reserve)
+            return tecINSUFFICIENT_RESERVE;
     }
 
     return insertRet;
diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp
index ef7c385acb..f191e04a47 100644
--- a/src/test/app/NFToken_test.cpp
+++ b/src/test/app/NFToken_test.cpp
@@ -6351,60 +6351,44 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             // Bob owns no object
             BEAST_EXPECT(ownerCount(env, bob) == 0);
 
-            // Without fixNFTokenReserve amendment, when bob accepts an NFT sell
-            // offer, he can get the NFT free of reserve
-            if (!features[fixNFTokenReserve])
-            {
-                // Bob is able to accept the offer
-                env(token::acceptSellOffer(bob, sellOfferIndex));
-                env.close();
-
-                // Bob now owns an extra objects
-                BEAST_EXPECT(ownerCount(env, bob) == 1);
-
-                // This is the wrong behavior, since Bob should need at least
-                // one incremental reserve.
-            }
-            // With fixNFTokenReserve, bob can no longer accept the offer unless
+            // bob can no longer accept the offer unless
             // there is enough reserve. A detail to note is that NFTs(sell
             // offer) will not allow one to go below the reserve requirement,
             // because buyer's balance is computed after the transaction fee is
             // deducted. This means that the reserve requirement will be `base
             // fee` drops higher than normal.
-            else
-            {
-                // Bob is not able to accept the offer with only the account
-                // reserve (200,000,000 drops)
-                env(token::acceptSellOffer(bob, sellOfferIndex), Ter(tecINSUFFICIENT_RESERVE));
-                env.close();
 
-                // after prev transaction, Bob owns `200M - base fee` drops due
-                // to burnt tx fee
+            // Bob is not able to accept the offer with only the account
+            // reserve (200,000,000 drops)
+            env(token::acceptSellOffer(bob, sellOfferIndex), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
 
-                BEAST_EXPECT(ownerCount(env, bob) == 0);
+            // after prev transaction, Bob owns `200M - base fee` drops due
+            // to burnt tx fee
 
-                // Send bob an kIncrement reserve and base fee (to make up for
-                // the transaction fee burnt from the prev failed tx) Bob now
-                // owns 250,000,000 drops
-                env(pay(env.master, bob, incReserve + drops(baseFee)));
-                env.close();
+            BEAST_EXPECT(ownerCount(env, bob) == 0);
 
-                // However, this transaction will still fail because the reserve
-                // requirement is `base fee` drops higher
-                env(token::acceptSellOffer(bob, sellOfferIndex), Ter(tecINSUFFICIENT_RESERVE));
-                env.close();
+            // Send bob an kIncrement reserve and base fee (to make up for
+            // the transaction fee burnt from the prev failed tx) Bob now
+            // owns 250,000,000 drops
+            env(pay(env.master, bob, incReserve + drops(baseFee)));
+            env.close();
 
-                // Send bob `base fee * 2` drops
-                // Bob now owns `250M + base fee` drops
-                env(pay(env.master, bob, drops(baseFee * 2)));
-                env.close();
+            // However, this transaction will still fail because the reserve
+            // requirement is `base fee` drops higher
+            env(token::acceptSellOffer(bob, sellOfferIndex), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
 
-                // Bob is now able to accept the offer
-                env(token::acceptSellOffer(bob, sellOfferIndex));
-                env.close();
+            // Send bob `base fee * 2` drops
+            // Bob now owns `250M + base fee` drops
+            env(pay(env.master, bob, drops(baseFee * 2)));
+            env.close();
 
-                BEAST_EXPECT(ownerCount(env, bob) == 1);
-            }
+            // Bob is now able to accept the offer
+            env(token::acceptSellOffer(bob, sellOfferIndex));
+            env.close();
+
+            BEAST_EXPECT(ownerCount(env, bob) == 1);
         }
 
         // Now exercise the scenario when the buyer accepts
@@ -6423,83 +6407,63 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
             env.fund(acctReserve + XRP(1), bob);
             env.close();
 
-            if (!features[fixNFTokenReserve])
+            // alice mints the first NFT and creates a sell offer for 0 XRP
+            auto const sellOfferIndex1 = mintAndCreateSellOffer(env, alice, XRP(0));
+
+            // Bob cannot accept this offer because he doesn't have the
+            // reserve for the NFT
+            env(token::acceptSellOffer(bob, sellOfferIndex1), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
+
+            // Give bob enough reserve
+            env(pay(env.master, bob, drops(incReserve)));
+            env.close();
+
+            BEAST_EXPECT(ownerCount(env, bob) == 0);
+
+            // Bob now owns his first NFT
+            env(token::acceptSellOffer(bob, sellOfferIndex1));
+            env.close();
+
+            BEAST_EXPECT(ownerCount(env, bob) == 1);
+
+            // alice now mints 31 more NFTs and creates an offer for each
+            // NFT, then sells to bob
+            for (size_t i = 0; i < 31; i++)
             {
-                // Bob can accept many NFTs without having a single reserve!
-                for (size_t i = 0; i < 200; i++)
-                {
-                    // alice mints an NFT and creates a sell offer for 0 XRP
-                    auto const sellOfferIndex = mintAndCreateSellOffer(env, alice, XRP(0));
+                // alice mints an NFT and creates a sell offer for 0 XRP
+                auto const sellOfferIndex = mintAndCreateSellOffer(env, alice, XRP(0));
 
-                    // Bob is able to accept the offer
-                    env(token::acceptSellOffer(bob, sellOfferIndex));
-                    env.close();
-                }
+                // Bob can accept the offer because the new NFT is stored in
+                // an existing NFTokenPage so no new reserve is required
+                env(token::acceptSellOffer(bob, sellOfferIndex));
+                env.close();
             }
-            else
-            {
-                // alice mints the first NFT and creates a sell offer for 0 XRP
-                auto const sellOfferIndex1 = mintAndCreateSellOffer(env, alice, XRP(0));
 
-                // Bob cannot accept this offer because he doesn't have the
-                // reserve for the NFT
-                env(token::acceptSellOffer(bob, sellOfferIndex1), Ter(tecINSUFFICIENT_RESERVE));
-                env.close();
+            BEAST_EXPECT(ownerCount(env, bob) == 1);
 
-                // Give bob enough reserve
-                env(pay(env.master, bob, drops(incReserve)));
-                env.close();
+            // alice now mints the 33rd NFT and creates an sell offer for 0
+            // XRP
+            auto const sellOfferIndex33 = mintAndCreateSellOffer(env, alice, XRP(0));
 
-                BEAST_EXPECT(ownerCount(env, bob) == 0);
+            // Bob fails to accept this NFT because he does not have enough
+            // reserve for a new NFTokenPage
+            env(token::acceptSellOffer(bob, sellOfferIndex33), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
 
-                // Bob now owns his first NFT
-                env(token::acceptSellOffer(bob, sellOfferIndex1));
-                env.close();
+            // Send bob incremental reserve
+            env(pay(env.master, bob, drops(incReserve)));
+            env.close();
 
-                BEAST_EXPECT(ownerCount(env, bob) == 1);
+            // Bob now has enough reserve to accept the offer and now
+            // owns one more NFTokenPage
+            env(token::acceptSellOffer(bob, sellOfferIndex33));
+            env.close();
 
-                // alice now mints 31 more NFTs and creates an offer for each
-                // NFT, then sells to bob
-                for (size_t i = 0; i < 31; i++)
-                {
-                    // alice mints an NFT and creates a sell offer for 0 XRP
-                    auto const sellOfferIndex = mintAndCreateSellOffer(env, alice, XRP(0));
-
-                    // Bob can accept the offer because the new NFT is stored in
-                    // an existing NFTokenPage so no new reserve is required
-                    env(token::acceptSellOffer(bob, sellOfferIndex));
-                    env.close();
-                }
-
-                BEAST_EXPECT(ownerCount(env, bob) == 1);
-
-                // alice now mints the 33rd NFT and creates an sell offer for 0
-                // XRP
-                auto const sellOfferIndex33 = mintAndCreateSellOffer(env, alice, XRP(0));
-
-                // Bob fails to accept this NFT because he does not have enough
-                // reserve for a new NFTokenPage
-                env(token::acceptSellOffer(bob, sellOfferIndex33), Ter(tecINSUFFICIENT_RESERVE));
-                env.close();
-
-                // Send bob incremental reserve
-                env(pay(env.master, bob, drops(incReserve)));
-                env.close();
-
-                // Bob now has enough reserve to accept the offer and now
-                // owns one more NFTokenPage
-                env(token::acceptSellOffer(bob, sellOfferIndex33));
-                env.close();
-
-                BEAST_EXPECT(ownerCount(env, bob) == 2);
-            }
+            BEAST_EXPECT(ownerCount(env, bob) == 2);
         }
 
         // Test the behavior when the seller accepts a buy offer.
-        // The behavior should not change regardless whether fixNFTokenReserve
-        // is enabled or not, since the ledger is able to guard against
-        // free NFTokenPages when buy offer is accepted. This is merely an
-        // additional test to exercise existing offer behavior.
         {
             Account const alice{"alice"};
             Account const bob{"bob"};
@@ -6544,10 +6508,6 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         }
 
         // Test the reserve behavior in brokered mode.
-        // The behavior should not change regardless whether fixNFTokenReserve
-        // is enabled or not, since the ledger is able to guard against
-        // free NFTokenPages in brokered mode. This is merely an
-        // additional test to exercise existing offer behavior.
         {
             Account const alice{"alice"};
             Account const bob{"bob"};
@@ -7211,9 +7171,7 @@ public:
     void
     run() override
     {
-        testWithFeats(
-            allFeatures_ - fixNFTokenReserve - featureNFTokenMintOffer - featureDynamicNFT -
-            fixCleanup3_1_3);
+        testWithFeats(allFeatures_ - featureNFTokenMintOffer - featureDynamicNFT - fixCleanup3_1_3);
     }
 };
 
@@ -7222,8 +7180,7 @@ class NFTokenDisallowIncoming_test : public NFTokenBaseUtil_test
     void
     run() override
     {
-        testWithFeats(
-            allFeatures_ - fixNFTokenReserve - featureNFTokenMintOffer - featureDynamicNFT);
+        testWithFeats(allFeatures_ - featureNFTokenMintOffer - featureDynamicNFT);
     }
 };
 

From bb2ab4243b81a056063f23d1bd3f8babe1f8b32b Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Fri, 26 Jun 2026 11:42:24 +0100
Subject: [PATCH 016/100] ci: Better determine when we need to run full
 clang-tidy (#7635)

---
 .github/workflows/on-pr.yml               |  1 -
 .github/workflows/on-trigger.yml          |  1 -
 .github/workflows/reusable-clang-tidy.yml | 11 +++--------
 3 files changed, 3 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml
index 2ad0641863..19fb170b92 100644
--- a/.github/workflows/on-pr.yml
+++ b/.github/workflows/on-pr.yml
@@ -122,7 +122,6 @@ jobs:
       issues: write
       contents: read
     with:
-      check_only_changed: true
       create_issue_on_failure: false
 
   build-test:
diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml
index 5f018cb12c..49a93d2746 100644
--- a/.github/workflows/on-trigger.yml
+++ b/.github/workflows/on-trigger.yml
@@ -72,7 +72,6 @@ jobs:
       issues: write
       contents: read
     with:
-      check_only_changed: false
       create_issue_on_failure: ${{ github.event_name == 'schedule' }}
 
   build-test:
diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml
index f36463a5d0..e66909ffad 100644
--- a/.github/workflows/reusable-clang-tidy.yml
+++ b/.github/workflows/reusable-clang-tidy.yml
@@ -3,10 +3,6 @@ name: Run clang-tidy on files
 on:
   workflow_call:
     inputs:
-      check_only_changed:
-        description: "Check only changed files in PR. If false, checks all files in the repository."
-        type: boolean
-        default: false
       create_issue_on_failure:
         description: "Whether to create an issue if the check failed"
         type: boolean
@@ -29,15 +25,14 @@ env:
 
 jobs:
   determine-files:
-    if: ${{ inputs.check_only_changed }}
     permissions:
       contents: read
-    uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@c7045074aafe9fb92fa537aa4446f81fbfc17e8b
+    uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@d041ac9f1fa9f07a4ba335eb4c1c82233fb3fef6
 
   run-clang-tidy:
     name: Run clang tidy
     needs: [determine-files]
-    if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }}
+    if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
     runs-on: ["self-hosted", "Linux", "X64", "heavy"]
     container: "ghcr.io/xrplf/xrpld/nix-debian:sha-e29b523"
     permissions:
@@ -96,7 +91,7 @@ jobs:
         id: run_clang_tidy
         continue-on-error: true
         env:
-          TARGETS: ${{ (needs.determine-files.outputs.clang_tidy_config_changed != 'true' && inputs.check_only_changed) && needs.determine-files.outputs.cpp_changed_files || 'src tests' }}
+          TARGETS: ${{ needs.determine-files.outputs.need_full_run != 'true' && needs.determine-files.outputs.cpp_changed_files || 'src tests' }}
         run: |
           set -o pipefail
           run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}"

From 50fdb38ace4faa3b8f75fde106c14c83d0637fb6 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Fri, 26 Jun 2026 11:46:39 +0100
Subject: [PATCH 017/100] chore: Enable groups of clang-tidy checks by default
 (#7637)

---
 .clang-tidy | 307 ++++++++++++++++++++++++++--------------------------
 1 file changed, 152 insertions(+), 155 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index 35427810a3..ef55e8517c 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -1,161 +1,158 @@
 ---
 Checks: "-*,
-  bugprone-argument-comment,
-  bugprone-assert-side-effect,
-  bugprone-bad-signal-to-kill-thread,
-  bugprone-bool-pointer-implicit-conversion,
-  bugprone-capturing-this-in-member-variable,
-  bugprone-casting-through-void,
-  bugprone-chained-comparison,
-  bugprone-compare-pointer-to-member-virtual-function,
-  bugprone-copy-constructor-init,
-  bugprone-crtp-constructor-accessibility,
-  bugprone-dangling-handle,
-  bugprone-derived-method-shadowing-base-method,
-  bugprone-dynamic-static-initializers,
-  bugprone-empty-catch,
-  bugprone-fold-init-type,
-  bugprone-forward-declaration-namespace,
-  bugprone-inaccurate-erase,
-  bugprone-inc-dec-in-conditions,
-  bugprone-incorrect-enable-if,
-  bugprone-incorrect-roundings,
-  bugprone-infinite-loop,
-  bugprone-integer-division,
-  bugprone-invalid-enum-default-initialization,
-  bugprone-lambda-function-name,
-  bugprone-macro-parentheses,
-  bugprone-macro-repeated-side-effects,
-  bugprone-misleading-setter-of-reference,
-  bugprone-misplaced-operator-in-strlen-in-alloc,
-  bugprone-misplaced-pointer-arithmetic-in-alloc,
-  bugprone-misplaced-widening-cast,
-  bugprone-move-forwarding-reference,
-  bugprone-multi-level-implicit-pointer-conversion,
-  bugprone-multiple-new-in-one-expression,
-  bugprone-multiple-statement-macro,
-  bugprone-no-escape,
-  bugprone-non-zero-enum-to-bool-conversion,
-  bugprone-optional-value-conversion,
-  bugprone-parent-virtual-call,
-  bugprone-pointer-arithmetic-on-polymorphic-object,
-  bugprone-posix-return,
-  bugprone-redundant-branch-condition,
-  bugprone-reserved-identifier,
-  bugprone-return-const-ref-from-parameter,
-  bugprone-shared-ptr-array-mismatch,
-  bugprone-signal-handler,
-  bugprone-signed-char-misuse,
-  bugprone-sizeof-container,
-  bugprone-sizeof-expression,
-  bugprone-spuriously-wake-up-functions,
-  bugprone-standalone-empty,
-  bugprone-string-constructor,
-  bugprone-string-integer-assignment,
-  bugprone-string-literal-with-embedded-nul,
-  bugprone-stringview-nullptr,
-  bugprone-suspicious-enum-usage,
-  bugprone-suspicious-include,
-  bugprone-suspicious-memory-comparison,
-  bugprone-suspicious-memset-usage,
-  bugprone-suspicious-missing-comma,
-  bugprone-suspicious-realloc-usage,
-  bugprone-suspicious-semicolon,
-  bugprone-suspicious-string-compare,
-  bugprone-suspicious-stringview-data-usage,
-  bugprone-swapped-arguments,
-  bugprone-switch-missing-default-case,
-  bugprone-terminating-continue,
-  bugprone-throw-keyword-missing,
-  bugprone-too-small-loop-variable,
-  bugprone-unchecked-optional-access,
-  bugprone-undefined-memory-manipulation,
-  bugprone-undelegated-constructor,
-  bugprone-unhandled-exception-at-new,
-  bugprone-unhandled-self-assignment,
-  bugprone-unique-ptr-array-mismatch,
-  bugprone-unsafe-functions,
-  bugprone-unused-local-non-trivial-variable,
-  bugprone-unused-raii,
-  bugprone-unused-return-value,
-  bugprone-use-after-move,
-  bugprone-virtual-near-miss,
-  cppcoreguidelines-init-variables,
-  cppcoreguidelines-misleading-capture-default-by-value,
-  cppcoreguidelines-no-suspend-with-lock,
-  cppcoreguidelines-pro-type-member-init,
-  cppcoreguidelines-pro-type-static-cast-downcast,
-  cppcoreguidelines-rvalue-reference-param-not-moved,
-  cppcoreguidelines-use-default-member-init,
-  cppcoreguidelines-use-enum-class,
-  cppcoreguidelines-virtual-class-destructor,
-  hicpp-ignored-remove-result,
+  bugprone-*,
+  -bugprone-assignment-in-if-condition,
+  -bugprone-bitwise-pointer-cast,
+  -bugprone-branch-clone,
+  -bugprone-command-processor,
+  -bugprone-copy-constructor-mutates-argument,
+  -bugprone-default-operator-new-on-overaligned-type,
+  -bugprone-easily-swappable-parameters,
+  -bugprone-exception-copy-constructor-throws,
+  -bugprone-exception-escape,
+  -bugprone-float-loop-counter,
+  -bugprone-forwarding-reference-overload,
+  -bugprone-implicit-widening-of-multiplication-result,
+  -bugprone-incorrect-enable-shared-from-this,
+  -bugprone-narrowing-conversions,
+  -bugprone-nondeterministic-pointer-iteration-order,
+  -bugprone-not-null-terminated-result,
+  -bugprone-random-generator-seed,
+  -bugprone-raw-memory-call-on-non-trivial-type,
+  -bugprone-std-namespace-modification,
+  -bugprone-tagged-union-member-count,
+  -bugprone-throwing-static-initialization,
+  -bugprone-unchecked-string-to-number-conversion,
+  -bugprone-unintended-char-ostream-output,
+
+  cppcoreguidelines-*,
+  -cppcoreguidelines-avoid-c-arrays,
+  -cppcoreguidelines-avoid-capturing-lambda-coroutines,
+  -cppcoreguidelines-avoid-const-or-ref-data-members,
+  -cppcoreguidelines-avoid-do-while,
+  -cppcoreguidelines-avoid-goto,
+  -cppcoreguidelines-avoid-magic-numbers,
+  -cppcoreguidelines-avoid-non-const-global-variables,
+  -cppcoreguidelines-avoid-reference-coroutine-parameters,
+  -cppcoreguidelines-c-copy-assignment-signature,
+  -cppcoreguidelines-explicit-virtual-functions,
+  -cppcoreguidelines-interfaces-global-init,
+  -cppcoreguidelines-macro-to-enum,
+  -cppcoreguidelines-macro-usage,
+  -cppcoreguidelines-missing-std-forward,
+  -cppcoreguidelines-narrowing-conversions,
+  -cppcoreguidelines-no-malloc,
+  -cppcoreguidelines-noexcept-destructor,
+  -cppcoreguidelines-noexcept-move-operations,
+  -cppcoreguidelines-noexcept-swap,
+  -cppcoreguidelines-non-private-member-variables-in-classes,
+  -cppcoreguidelines-owning-memory,
+  -cppcoreguidelines-prefer-member-initializer,
+  -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
+  -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
+  -cppcoreguidelines-pro-bounds-constant-array-index,
+  -cppcoreguidelines-pro-bounds-pointer-arithmetic,
+  -cppcoreguidelines-pro-type-const-cast,
+  -cppcoreguidelines-pro-type-cstyle-cast,
+  -cppcoreguidelines-pro-type-reinterpret-cast,
+  -cppcoreguidelines-pro-type-union-access,
+  -cppcoreguidelines-pro-type-vararg,
+  -cppcoreguidelines-slicing,
+  -cppcoreguidelines-special-member-functions,
+
   llvm-namespace-comment,
-  misc-const-correctness,
-  misc-definitions-in-headers,
-  misc-header-include-cycle,
-  misc-include-cleaner,
-  misc-misplaced-const,
-  misc-redundant-expression,
-  misc-static-assert,
-  misc-throw-by-value-catch-by-reference,
-  misc-unused-alias-decls,
-  misc-unused-using-decls,
-  modernize-concat-nested-namespaces,
-  modernize-deprecated-headers,
-  modernize-make-shared,
-  modernize-make-unique,
-  modernize-pass-by-value,
-  modernize-type-traits,
-  modernize-use-designated-initializers,
-  modernize-use-emplace,
-  modernize-use-equals-default,
-  modernize-use-equals-delete,
-  modernize-use-nodiscard,
-  modernize-use-override,
-  modernize-use-ranges,
-  modernize-use-scoped-lock,
-  modernize-use-starts-ends-with,
-  modernize-use-std-numbers,
-  modernize-use-using,
-  performance-faster-string-find,
-  performance-for-range-copy,
-  performance-implicit-conversion-in-loop,
-  performance-inefficient-vector-operation,
-  performance-move-const-arg,
-  performance-move-constructor-init,
-  performance-no-automatic-move,
-  performance-trivially-destructible,
-  readability-ambiguous-smartptr-reset-call,
-  readability-avoid-nested-conditional-operator,
-  readability-avoid-return-with-void-value,
-  readability-braces-around-statements,
-  readability-const-return-type,
-  readability-container-contains,
-  readability-container-size-empty,
-  readability-convert-member-functions-to-static,
-  readability-duplicate-include,
-  readability-else-after-return,
-  readability-enum-initial-value,
-  readability-identifier-naming,
-  readability-implicit-bool-conversion,
-  readability-inconsistent-ifelse-braces,
-  readability-make-member-function-const,
-  readability-math-missing-parentheses,
-  readability-misleading-indentation,
-  readability-non-const-parameter,
-  readability-redundant-casting,
-  readability-redundant-declaration,
-  readability-redundant-inline-specifier,
-  readability-redundant-member-init,
-  readability-redundant-parentheses,
-  readability-redundant-string-init,
-  readability-redundant-typename,
-  readability-reference-to-constructed-temporary,
-  readability-simplify-boolean-expr,
-  readability-static-definition-in-anonymous-namespace,
-  readability-suspicious-call-argument,
-  readability-use-std-min-max
+
+  misc-*,
+  -misc-anonymous-namespace-in-header,
+  -misc-confusable-identifiers,
+  -misc-coroutine-hostile-raii,
+  -misc-misleading-bidirectional,
+  -misc-misleading-identifier,
+  -misc-multiple-inheritance,
+  -misc-new-delete-overloads,
+  -misc-no-recursion,
+  -misc-non-copyable-objects,
+  -misc-non-private-member-variables-in-classes,
+  -misc-override-with-different-visibility,
+  -misc-predictable-rand,
+  -misc-unconventional-assign-operator,
+  -misc-uniqueptr-reset-release,
+  -misc-unused-parameters,
+  -misc-use-anonymous-namespace,
+  -misc-use-internal-linkage,
+
+  modernize-*,
+  -modernize-avoid-bind,
+  -modernize-avoid-c-arrays,
+  -modernize-avoid-c-style-cast,
+  -modernize-avoid-setjmp-longjmp,
+  -modernize-avoid-variadic-functions,
+  -modernize-deprecated-ios-base-aliases,
+  -modernize-loop-convert,
+  -modernize-macro-to-enum,
+  -modernize-min-max-use-initializer-list,
+  -modernize-raw-string-literal,
+  -modernize-redundant-void-arg,
+  -modernize-replace-auto-ptr,
+  -modernize-replace-disallow-copy-and-assign-macro,
+  -modernize-replace-random-shuffle,
+  -modernize-return-braced-init-list,
+  -modernize-shrink-to-fit,
+  -modernize-unary-static-assert,
+  -modernize-use-auto,
+  -modernize-use-bool-literals,
+  -modernize-use-constraints,
+  -modernize-use-default-member-init,
+  -modernize-use-integer-sign-comparison,
+  -modernize-use-noexcept,
+  -modernize-use-nullptr,
+  -modernize-use-std-format,
+  -modernize-use-std-print,
+  -modernize-use-trailing-return-type,
+  -modernize-use-transparent-functors,
+  -modernize-use-uncaught-exceptions,
+
+  performance-*,
+  -performance-avoid-endl,
+  -performance-enum-size,
+  -performance-inefficient-algorithm,
+  -performance-inefficient-string-concatenation,
+  -performance-no-int-to-ptr,
+  -performance-noexcept-destructor,
+  -performance-noexcept-move-constructor,
+  -performance-noexcept-swap,
+  -performance-type-promotion-in-math-fn,
+  -performance-unnecessary-copy-initialization,
+  -performance-unnecessary-value-param,
+
+  readability-*,
+  -readability-avoid-const-params-in-decls,
+  -readability-avoid-unconditional-preprocessor-if,
+  -readability-container-data-pointer,
+  -readability-delete-null-pointer,
+  -readability-function-cognitive-complexity,
+  -readability-function-size,
+  -readability-identifier-length,
+  -readability-inconsistent-declaration-parameter-name,
+  -readability-isolate-declaration,
+  -readability-magic-numbers,
+  -readability-misplaced-array-index,
+  -readability-named-parameter,
+  -readability-operators-representation,
+  -readability-qualified-auto,
+  -readability-redundant-access-specifiers,
+  -readability-redundant-control-flow,
+  -readability-redundant-function-ptr-dereference,
+  -readability-redundant-preprocessor,
+  -readability-redundant-smartptr-get,
+  -readability-redundant-string-cstr,
+  -readability-simplify-subscript-expr,
+  -readability-static-accessed-through-instance,
+  -readability-string-compare,
+  -readability-uniqueptr-delete-release,
+  -readability-uppercase-literal-suffix,
+  -readability-use-anyofallof,
+  -readability-use-concise-preprocessor-directives
   "
 # ---
 # bugprone-narrowing-conversions,                      # This will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs

From 652b5f9af1b58c974ddcb8c4684227aca9fd1527 Mon Sep 17 00:00:00 2001
From: yinyiqian1 
Date: Fri, 26 Jun 2026 16:34:22 -0400
Subject: [PATCH 018/100] fix: Block delegate tx from being queued (#7640)

---
 src/test/app/TxQ_test.cpp         | 38 +++++++++++++++++++++++++++++++
 src/xrpld/app/misc/detail/TxQ.cpp |  4 ++++
 2 files changed, 42 insertions(+)

diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp
index 97cb035578..2d10eb4beb 100644
--- a/src/test/app/TxQ_test.cpp
+++ b/src/test/app/TxQ_test.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -2335,6 +2336,42 @@ public:
         BEAST_EXPECT(env.balance(alice) == drops(5));
     }
 
+    void
+    testDelegateTxCannotQueue()
+    {
+        using namespace jtx;
+        testcase("disallow delegate transaction from being queued");
+
+        Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}}));
+
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const carol = Account("carol");
+
+        env.fund(XRP(50000), alice, bob);
+        env.close();
+        env.fund(XRP(50000), carol);
+        env.close();
+
+        env(delegate::set(alice, bob, {"Payment"}));
+        env.close();
+
+        fillQueue(env, alice);
+        checkMetrics(*this, env, 0, 8, 5, 4);
+
+        // Delegated transactions are not allowed to be queued.
+        env(pay(alice, carol, drops(1)), delegate::As(bob), Ter(telCAN_NOT_QUEUE));
+        checkMetrics(*this, env, 0, 8, 5, 4);
+
+        // Delegated transactions may still apply directly if they pay the
+        // open ledger fee. They just cannot be held in the queue.
+        env(pay(alice, carol, drops(1)),
+            delegate::As(bob),
+            Fee(openLedgerCost(env)),
+            Ter(tesSUCCESS));
+        checkMetrics(*this, env, 0, 8, 6, 4);
+    }
+
     void
     testConsequences()
     {
@@ -4662,6 +4699,7 @@ public:
         testBlockersSeq();
         testBlockersTicket();
         testInFlightBalance();
+        testDelegateTxCannotQueue();
         testConsequences();
     }
 
diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp
index 9022095ea8..ec5ac569d6 100644
--- a/src/xrpld/app/misc/detail/TxQ.cpp
+++ b/src/xrpld/app/misc/detail/TxQ.cpp
@@ -398,6 +398,10 @@ TxQ::canBeHeld(
         ((flags & TapFailHard) != 0u))
         return telCAN_NOT_QUEUE;
 
+    // Disallow delegated transactions from being queued.
+    if (tx.isFieldPresent(sfDelegate))
+        return telCAN_NOT_QUEUE;
+
     {
         // To be queued and relayed, the transaction needs to
         // promise to stick around for long enough that it has

From 3e9f1d0ab856775ee5a1fc3cf5e2ee942b508821 Mon Sep 17 00:00:00 2001
From: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Date: Fri, 26 Jun 2026 23:38:59 +0200
Subject: [PATCH 019/100] fix: Unify freeze checks for pseudo-account
 deposit/withdraw (#7382)

Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov 
Co-authored-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
Co-authored-by: Shawn Xie <35279399+shawnxie999@users.noreply.github.com>
---
 include/xrpl/ledger/helpers/TokenHelpers.h    |  69 ++
 include/xrpl/tx/transactors/dex/AMMWithdraw.h |   5 +
 src/libxrpl/ledger/helpers/MPTokenHelpers.cpp |  26 +-
 src/libxrpl/ledger/helpers/TokenHelpers.cpp   |  96 +-
 src/libxrpl/tx/invariants/MPTInvariant.cpp    |  40 +-
 src/libxrpl/tx/transactors/dex/AMMDeposit.cpp |  68 +-
 .../tx/transactors/dex/AMMWithdraw.cpp        |  67 +-
 .../lending/LoanBrokerCoverDeposit.cpp        |  24 +-
 .../lending/LoanBrokerCoverWithdraw.cpp       |  35 +-
 .../tx/transactors/vault/VaultDeposit.cpp     |  24 +-
 .../tx/transactors/vault/VaultWithdraw.cpp    |  53 +-
 src/test/app/AMMMPT_test.cpp                  | 222 ++++-
 src/test/app/AMM_test.cpp                     | 145 ++-
 src/test/app/Invariants_test.cpp              | 205 ++++
 src/test/app/LoanBroker_test.cpp              | 476 ++++++++-
 src/test/app/Loan_test.cpp                    |  21 +-
 src/test/app/MPToken_test.cpp                 |  51 +-
 src/test/app/Vault_test.cpp                   | 943 +++++++++++-------
 src/test/jtx/Env.h                            |  20 +
 src/test/jtx/impl/utility.cpp                 |   1 -
 src/test/jtx/utility.h                        |   2 +-
 21 files changed, 2007 insertions(+), 586 deletions(-)

diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h
index f736e51d28..0c9871cd76 100644
--- a/include/xrpl/ledger/helpers/TokenHelpers.h
+++ b/include/xrpl/ledger/helpers/TokenHelpers.h
@@ -131,6 +131,75 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const&
 [[nodiscard]] TER
 checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
 
+/**
+ * Checks freeze compliance for withdrawing an asset from a pseudo-account (e.g. Vault, AMM,
+ * LoanBroker) to a destination account.
+ *
+ * Asserts that sourceAcct is a pseudo-account and that submitterAcct and dstAcct are not.
+ *
+ * Issuer exemption: returns tesSUCCESS immediately when dstAcct is the asset issuer — the issuer
+ * can always receive their own token, even when the pool is frozen.  Callers that need to block
+ * withdrawals from a frozen pool even for the issuer (e.g. because the pool math cannot handle it)
+ * must check checkFrozen(sourceAcct, asset) separately before calling this function.
+ *
+ * Otherwise checks, in order:
+ *   1. If the asset is globally frozen the remaining checks are redundant.
+ *   2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its
+ * underlying asset.
+ *   3. The pseudo-account's trustline / MPToken must not be frozen for sending.
+ *   4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent
+ * recovering one's own funds.
+ *   5. The destination must not be deep-frozen (cannot receive under any circumstance).
+ *
+ * For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep
+ * freeze does.  For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
+ * blocked.
+ *
+ * @param view          Ledger view to read freeze state from.
+ * @param srcAcct       Pseudo-account the funds are withdrawn from (sender).
+ * @param submitterAcct Account that submitted the withdrawal transaction.
+ * @param dstAcct       Account receiving the withdrawn funds.
+ * @param asset         Asset being withdrawn.
+ * @return tesSUCCESS if the withdrawal is permitted, otherwise a freeze
+ *         result (tecFROZEN for IOUs, tecLOCKED for MPTs).
+ */
+[[nodiscard]] TER
+checkWithdrawFreeze(
+    ReadView const& view,
+    AccountID const& srcAcct,
+    AccountID const& submitterAcct,
+    AccountID const& dstAcct,
+    Asset const& asset);
+
+/**
+ * Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM,
+ * LoanBroker).
+ *
+ *
+ * Checks, in order:
+ *   1. If the asset is globally frozen the remaining checks are redundant.
+ *   2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its
+ * underlying asset (returns tecLOCKED).
+ *   3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer,
+ * since the issuer can always send its own asset.
+ *   4. The pseudo-account must not be individually frozen for the asset.  Unlike regular accounts,
+ * pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds
+ * could not later be withdrawn.
+ *
+ * @param view    Ledger view to read freeze state from.
+ * @param srcAcct Depositor sending the funds.
+ * @param dstAcct Pseudo-account receiving the deposit.
+ * @param asset   Asset being deposited.
+ * @return tesSUCCESS if the deposit is permitted, otherwise a freeze result
+ *         (tecFROZEN for IOUs, tecLOCKED for MPTs).
+ */
+[[nodiscard]] TER
+checkDepositFreeze(
+    ReadView const& view,
+    AccountID const& srcAcct,
+    AccountID const& dstAcct,
+    Asset const& asset);
+
 //------------------------------------------------------------------------------
 //
 // Account balance functions (Asset-based dispatchers)
diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h
index 6e88320eae..8b6d39349b 100644
--- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h
+++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h
@@ -159,6 +159,11 @@ public:
         beast::Journal const& journal);
 
 private:
+    /** Returns IgnoreFreeze when the withdrawer is the issuer of a pool
+     *  asset (post-fixCleanup3_3_0), ZeroIfFrozen otherwise. */
+    [[nodiscard]] FreezeHandling
+    issuerFreezeHandling() const;
+
     std::pair
     applyGuts(Sandbox& view);
 
diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
index a1af63a80f..2781902f5b 100644
--- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
@@ -308,6 +308,18 @@ requireAuth(
     AuthType authType,
     std::uint8_t depth)
 {
+    bool const fix330Enabled = view.rules().enabled(fixCleanup3_3_0);
+    bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault);
+    bool const featureMPTV2Enabled = view.rules().enabled(featureMPTokensV2);
+
+    // Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their participants.
+    // They are implicitly authorized for any MPT they hold, including vault shares whose
+    // underlying asset would otherwise require auth.
+    auto const isPseudoAccountExempt = [&] {
+        return (featureSAVEnabled || featureMPTV2Enabled) &&
+            isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID});
+    };
+
     auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
     auto const sleIssuance = view.read(mptID);
     if (!sleIssuance)
@@ -319,7 +331,9 @@ requireAuth(
     if (mptIssuer == account)  // Issuer won't have MPToken
         return tesSUCCESS;
 
-    bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault);
+    // Post-fix330: exempt before the recursive underlying-asset auth check.
+    if (fix330Enabled && isPseudoAccountExempt())
+        return tesSUCCESS;
 
     if (featureSAVEnabled)
     {
@@ -382,13 +396,9 @@ requireAuth(
         // belong to someone who is explicitly authorized e.g. a vault owner.
     }
 
-    bool const featureMPTV2Enabled = view.rules().enabled(featureMPTokensV2);
-    if (featureSAVEnabled || featureMPTV2Enabled)
-    {
-        // Implicitly authorize Vault, LoanBroker, and AMM pseudo-accounts
-        if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
-            return tesSUCCESS;
-    }
+    // Pre-fix330: exempt after domain/sleToken checks, preserving prior behavior.
+    if (!fix330Enabled && isPseudoAccountExempt())
+        return tesSUCCESS;
 
     // mptoken must be authorized if issuance enabled requireAuth
     if (sleIssuance->isFlag(lsfMPTRequireAuth) &&
diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
index f0a0220e1e..b9adfbd3e6 100644
--- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
@@ -6,6 +6,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -34,14 +35,6 @@
 
 namespace xrpl {
 
-// Forward declaration for function that remains in View.h/cpp
-bool
-isLPTokenFrozen(
-    ReadView const& view,
-    AccountID const& account,
-    Asset const& asset,
-    Asset const& asset2);
-
 //------------------------------------------------------------------------------
 //
 // Freeze checking (Asset-based)
@@ -164,6 +157,90 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
         [&](auto const& issue) { return checkDeepFrozen(view, account, issue); }, asset.value());
 }
 
+[[nodiscard]] TER
+checkWithdrawFreeze(
+    ReadView const& view,
+    AccountID const& srcAcct,
+    AccountID const& submitterAcct,
+    AccountID const& dstAcct,
+    Asset const& asset)
+{
+    XRPL_ASSERT(
+        isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source is a pseudo-account");
+    XRPL_ASSERT(
+        !isPseudoAccount(view, submitterAcct),
+        "xrpl::checkWithdrawFreeze : submitter is not a pseudo-account");
+    XRPL_ASSERT(
+        !isPseudoAccount(view, dstAcct),
+        "xrpl::checkWithdrawFreeze : destination is not a pseudo-account");
+
+    // Funds can always be sent to the issuer
+    if (dstAcct == asset.getIssuer())
+        return tesSUCCESS;
+
+    // If the asset is globally frozen, other checks are redundant
+    if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret))
+        return ret;
+
+    // Special case for shares - check if the shares (and the transitive asset) is not frozen
+    if (asset.holds() &&
+        isVaultPseudoAccountFrozen(view, srcAcct, asset.get(), 0))
+    {
+        return tecLOCKED;
+    }
+
+    // The transfer is from Submitter to Destination via Source (pseudo-account)
+    // Both Source and Submitter must not be frozen to allow sending funds
+    if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
+        return ret;
+
+    // Check submitter's individual freeze only when Submitter != Destination (a regular freeze
+    // should not block self-withdrawal).
+    if (submitterAcct != dstAcct)
+    {
+        if (auto const ret = checkIndividualFrozen(view, submitterAcct, asset); !isTesSuccess(ret))
+            return ret;
+    }
+
+    // The destination account must not be deep frozen to receive the funds
+    return checkDeepFrozen(view, dstAcct, asset);
+}
+
+[[nodiscard]] TER
+checkDepositFreeze(
+    ReadView const& view,
+    AccountID const& srcAcct,
+    AccountID const& dstAcct,
+    Asset const& asset)
+{
+    XRPL_ASSERT(
+        isPseudoAccount(view, dstAcct),
+        "xrpl::checkDepositFreeze : destination is a pseudo-account");
+    XRPL_ASSERT(
+        !isPseudoAccount(view, srcAcct),
+        "xrpl::checkDepositFreeze : source is not a pseudo-account");
+
+    if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret))
+        return ret;
+
+    // Special case for shares - check if the shares and the transitive asset is not frozen
+    if (asset.holds() &&
+        isVaultPseudoAccountFrozen(view, dstAcct, asset.get(), 0))
+    {
+        return tecLOCKED;
+    }
+
+    if (srcAcct != asset.getIssuer())
+    {
+        if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
+            return ret;
+    }
+
+    // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze
+    // because those funds cannot be later withdrawn
+    return checkIndividualFrozen(view, dstAcct, asset);
+}
+
 //------------------------------------------------------------------------------
 //
 // Account balance functions
@@ -776,7 +853,8 @@ directSendNoLimitMultiIOU(
         if (senderID == issuer || receiverID == issuer || issuer == noAccount())
         {
             // Direct send: redeeming IOUs and/or sending own IOUs.
-            if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j))
+            if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j);
+                !isTesSuccess(ter))
                 return ter;
             actual += amount;
             // Do not add amount to takeFromSender, because directSendNoFeeIOU took
diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp
index 1176594bbf..278c7a7858 100644
--- a/src/libxrpl/tx/invariants/MPTInvariant.cpp
+++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp
@@ -4,6 +4,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -509,6 +511,15 @@ ValidMPTTransfer::isAuthorized(
     AccountID const& holder,
     bool reqAuth) const
 {
+    // Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their
+    // participants and are implicitly authorized for any MPT they hold,
+    // including vault shares whose underlying asset would otherwise require
+    // auth.  Exempt them here rather than relying on requireAuth: the recursive
+    // share -> underlying descent in requireAuth fails for a pseudo-account
+    // that holds the share but not the underlying.
+    if (isPseudoAccount(view, holder, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
+        return true;
+
     auto const key = keylet::mptoken(mptid, holder);
     auto const it = deletedAuthorized_.find(key.key);
     if (it != deletedAuthorized_.end())
@@ -524,6 +535,8 @@ ValidMPTTransfer::finalize(
     ReadView const& view,
     beast::Journal const& j)
 {
+    auto const fix330Enabled = view.rules().enabled(fixCleanup3_3_0);
+
     if (hasPrivilege(tx, OverrideFreeze))
         return true;
 
@@ -584,12 +597,29 @@ ValidMPTTransfer::finalize(
                     ++senders;
                 }
 
-                // Check once: if any involved account is frozen, the whole
-                // issuance transfer is considered frozen. Only need to check for
-                // frozen if there is a transfer of funds.
+                // Check once: if any involved account is frozen, the whole issuance transfer is
+                // considered frozen. Only need to check for frozen if there is a transfer of funds.
+                //
+                // Post-fix330: full isFrozen() applies — vault-share transitive freeze is part of
+                // the freeze semantics for all changed holders.
+                //
+                // Pre-fix330: legacy AMM withdraw only checked individual freeze on the
+                // destination, not the transitive vault freeze.  All other paths (and the AMM
+                // account itself as sender) did apply the full check.
+                MPTIssue const issue{mptID};
+                auto const legacyAccountFrozen = [&] {
+                    if (isGlobalFrozen(view, issue) || isIndividualFrozen(view, account, issue))
+                        return true;
+                    bool const isReceiver =
+                        !value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore;
+                    if (txnType == ttAMM_WITHDRAW && isReceiver)
+                        return false;
+                    return isVaultPseudoAccountFrozen(view, account, issue, 0);
+                };
+                bool const accountFrozen =
+                    fix330Enabled ? isFrozen(view, account, issue) : legacyAccountFrozen();
                 if (!invalidTransfer &&
-                    (isFrozen(view, account, MPTIssue{mptID}) ||
-                     !isAuthorized(view, mptID, account, reqAuth)))
+                    (accountFrozen || !isAuthorized(view, mptID, account, reqAuth)))
                 {
                     invalidTransfer = true;
                 }
diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
index 3665850f2d..ff7fec019a 100644
--- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
@@ -251,7 +251,36 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
             : tecUNFUNDED_AMM;
     };
 
-    if (ctx.view.rules().enabled(featureAMMClawback))
+    auto const amount = ctx.tx[~sfAmount];
+    auto const amount2 = ctx.tx[~sfAmount2];
+    auto const ammAccountID = ammSle->getAccountID(sfAccount);
+
+    if (ctx.view.rules().enabled(fixCleanup3_3_0))
+    {
+        // Unified deposit freeze check for both pool assets.
+        // AMMDeposit is not allowed if either asset is frozen.
+        auto checkAsset = [&](Asset const& asset) -> TER {
+            if (auto const ter = requireAuth(ctx.view, asset, accountID, AuthType::WeakAuth))
+            {
+                JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " << asset;
+                return ter;
+            }
+            if (auto const ter = checkDepositFreeze(ctx.view, accountID, ammAccountID, asset))
+            {
+                JLOG(ctx.j.debug())
+                    << "AMM Deposit: frozen, " << to_string(accountID) << " " << to_string(asset);
+                return ter;
+            }
+            return tesSUCCESS;
+        };
+
+        if (auto const ter = checkAsset(ctx.tx[sfAsset]))
+            return ter;
+
+        if (auto const ter = checkAsset(ctx.tx[sfAsset2]))
+            return ter;
+    }
+    else if (ctx.view.rules().enabled(featureAMMClawback))
     {
         // Check if either of the assets is frozen, AMMDeposit is not allowed
         // if either asset is frozen
@@ -283,10 +312,6 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
             return ter;
     }
 
-    auto const amount = ctx.tx[~sfAmount];
-    auto const amount2 = ctx.tx[~sfAmount2];
-    auto const ammAccountID = ammSle->getAccountID(sfAccount);
-
     auto checkAmount = [&](std::optional const& amount, bool checkBalance) -> TER {
         if (amount)
         {
@@ -301,21 +326,26 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
                 return ter;
                 // LCOV_EXCL_STOP
             }
-            // AMM account or currency frozen
-            if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
-                !isTesSuccess(ter))
+            if (!ctx.view.rules().enabled(fixCleanup3_3_0))
             {
-                JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen or locked, "
-                                    << to_string(accountID);
-                return ter;
-            }
-            // Account frozen
-            if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
-                !isTesSuccess(ter))
-            {
-                JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen or locked, "
-                                    << to_string(accountID) << " " << to_string(amount->asset());
-                return ter;
+                // AMM account or currency frozen
+                if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
+                    !isTesSuccess(ter))
+                {
+                    JLOG(ctx.j.debug())
+                        << "AMM Deposit: AMM account or currency is frozen or locked, "
+                        << to_string(accountID);
+                    return ter;
+                }
+                // Account frozen
+                if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
+                    !isTesSuccess(ter))
+                {
+                    JLOG(ctx.j.debug())
+                        << "AMM Deposit: account is frozen or locked, " << to_string(accountID)
+                        << " " << to_string(amount->asset());
+                    return ter;
+                }
             }
             if (checkBalance)
             {
diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
index 6cbcfb1e50..f26164c5fe 100644
--- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
@@ -238,21 +238,36 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
                     << "AMM Withdraw: account is not authorized, " << amount->asset();
                 return ter;
             }
-            // AMM account or currency frozen
-            if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
-                !isTesSuccess(ter))
+            if (ctx.view.rules().enabled(fixCleanup3_3_0))
             {
-                JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen or locked, "
-                                    << to_string(accountID);
-                return ter;
+                if (auto const ret = checkWithdrawFreeze(
+                        ctx.view, ammAccountID, accountID, accountID, amount->asset()))
+                {
+                    JLOG(ctx.j.debug()) << "AMM Withdraw: frozen, " << to_string(accountID) << " "
+                                        << to_string(amount->asset());
+                    return ret;
+                }
             }
-            // Account frozen
-            if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
-                !isTesSuccess(ter))
+            else
             {
-                JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen or locked, "
-                                    << to_string(accountID) << " " << to_string(amount->asset());
-                return ter;
+                // AMM account or currency frozen
+                if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset());
+                    !isTesSuccess(ter))
+                {
+                    JLOG(ctx.j.debug())
+                        << "AMM Withdraw: AMM account or currency is frozen or locked, "
+                        << to_string(accountID);
+                    return ter;
+                }
+                // Account frozen
+                if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset());
+                    !isTesSuccess(ter))
+                {
+                    JLOG(ctx.j.debug())
+                        << "AMM Withdraw: account is frozen or locked, " << to_string(accountID)
+                        << " " << to_string(amount->asset());
+                    return ter;
+                }
             }
         }
         return tesSUCCESS;
@@ -302,6 +317,25 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
     return tesSUCCESS;
 }
 
+FreezeHandling
+AMMWithdraw::issuerFreezeHandling() const
+{
+    // When the withdrawer is the issuer of a pool asset, the issuer can
+    // always receive their own token — even when the pool is frozen.
+    // Use IgnoreFreeze so ammHolds returns real balances instead of zero.
+    if (!ctx_.view().rules().enabled(fixCleanup3_3_0))
+        return FreezeHandling::ZeroIfFrozen;
+
+    auto const asset1 = Asset{ctx_.tx[sfAsset]};
+    auto const asset2 = Asset{ctx_.tx[sfAsset2]};
+    if (!asset1.native() && accountID_ == asset1.getIssuer())
+        return FreezeHandling::IgnoreFreeze;
+    if (!asset2.native() && accountID_ == asset2.getIssuer())
+        return FreezeHandling::IgnoreFreeze;
+
+    return FreezeHandling::ZeroIfFrozen;
+}
+
 std::pair
 AMMWithdraw::applyGuts(Sandbox& sb)
 {
@@ -329,18 +363,19 @@ AMMWithdraw::applyGuts(Sandbox& sb)
 
     auto const tfee = getTradingFee(ctx_.view(), *ammSle, accountID_);
 
+    auto const freezeHandling = issuerFreezeHandling();
+
     auto const expected = ammHolds(
         sb,
         *ammSle,
         amount ? amount->asset() : std::optional{},
         amount2 ? amount2->asset() : std::optional{},
-        FreezeHandling::ZeroIfFrozen,
+        freezeHandling,
         AuthHandling::ZeroIfUnauthorized,
         ctx_.journal);
     if (!expected)
         return {expected.error(), false};
     auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected;
-
     auto const subTxType = ctx_.tx.getFlags() & tfWithdrawSubTx;
 
     auto const [result, newLPTokenBalance] = [&,
@@ -469,7 +504,7 @@ AMMWithdraw::withdraw(
         lpTokensAMMBalance,
         lpTokensWithdraw,
         tfee,
-        FreezeHandling::ZeroIfFrozen,
+        issuerFreezeHandling(),
         AuthHandling::ZeroIfUnauthorized,
         isWithdrawAll(ctx_.tx),
         preFeeBalance_,
@@ -756,7 +791,7 @@ AMMWithdraw::equalWithdrawTokens(
         lpTokens,
         lpTokensWithdraw,
         tfee,
-        FreezeHandling::ZeroIfFrozen,
+        issuerFreezeHandling(),
         AuthHandling::ZeroIfUnauthorized,
         isWithdrawAll(ctx_.tx),
         preFeeBalance_,
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp
index 76a5493a73..69b28b57af 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp
@@ -43,6 +43,8 @@ LoanBrokerCoverDeposit::preflight(PreflightContext const& ctx)
 TER
 LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
 {
+    auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
+    auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0);
     auto const& tx = ctx.tx;
 
     auto const account = tx[sfAccount];
@@ -77,12 +79,21 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
     // Cannot transfer a non-transferable Asset
     if (auto const ret = canTransfer(ctx.view, vaultAsset, account, pseudoAccountID))
         return ret;
-    // Cannot transfer a frozen Asset
-    if (auto const ret = checkFrozen(ctx.view, account, vaultAsset))
-        return ret;
-    // Pseudo-account cannot receive if asset is deep frozen
-    if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset))
-        return ret;
+
+    if (fix330Enabled)
+    {
+        if (auto const ret = checkDepositFreeze(ctx.view, account, pseudoAccountID, vaultAsset))
+            return ret;
+    }
+    else
+    {
+        if (auto const ret = checkFrozen(ctx.view, account, vaultAsset))
+            return ret;
+
+        if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset))
+            return ret;
+    }
+
     // Cannot transfer unauthorized asset
     if (auto const ret = requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth))
         return ret;
@@ -92,7 +103,6 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
     // `sfCoverAvailable  +=` could credit the broker more than the depositor paid  Computing it
     // here in preclaim lets  us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
     // failing only in  doApply.
-    bool const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
     auto const roundedAmount = [&]() -> STAmount {
         if (!fix320Enabled)
             return tx[sfAmount];
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
index 426f77cc70..008857a4ad 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
@@ -55,6 +55,8 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx)
 TER
 LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
 {
+    auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
+    auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0);
     auto const& tx = ctx.tx;
 
     auto const account = tx[sfAccount];
@@ -103,8 +105,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
     // the lsfMPTCanTransfer flag check, so an issuer cannot trap a broker's
     // first-loss capital. Other transferability checks (IOU NoRipple, freeze,
     // requireAuth) still apply.
-    auto const waive = ctx.view.rules().enabled(fixCleanup3_2_0) ? WaiveMPTCanTransfer::Yes
-                                                                 : WaiveMPTCanTransfer::No;
+    auto const waive = fix320Enabled ? WaiveMPTCanTransfer::Yes : WaiveMPTCanTransfer::No;
     if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct, waive))
         return ret;
 
@@ -125,22 +126,30 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
     if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType))
         return ter;
 
-    // Check for freezes, unless sending directly to the issuer
-    if (dstAcct != vaultAsset.getIssuer())
+    if (fix330Enabled)
     {
-        // Cannot send a frozen Asset
-        if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset))
-            return ret;
-        // Destination account cannot receive if asset is deep frozen
-        if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
+        if (auto const ret =
+                checkWithdrawFreeze(ctx.view, pseudoAccountID, account, dstAcct, vaultAsset))
             return ret;
     }
+    else
+    {  // Check for freezes, unless sending directly to the issuer
+        if (dstAcct != vaultAsset.getIssuer())
+        {
+            // Cannot send a frozen Asset
+            if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset))
+                return ret;
+            // Destination account cannot receive if asset is deep frozen
+            if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
+                return ret;
+        }
+    }
 
     auto const coverAvail = sleBroker->at(sfCoverAvailable);
     // Cover Rate is in 1/10 bips units
     auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
     auto const minimumCover = [&]() {
-        if (ctx.view.rules().enabled(fixCleanup3_2_0))
+        if (fix320Enabled)
         {
             return minimumBrokerCover(
                 currentDebtTotal, TenthBips32{sleBroker->at(sfCoverRateMinimum)}, vault);
@@ -159,11 +168,15 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
     if ((coverAvail - amount) < minimumCover)
         return tecINSUFFICIENT_FUNDS;
 
+    auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
+        ? FreezeHandling::IgnoreFreeze
+        : FreezeHandling::ZeroIfFrozen;
+
     if (accountHolds(
             ctx.view,
             pseudoAccountID,
             vaultAsset,
-            FreezeHandling::ZeroIfFrozen,
+            freezeHandling,
             AuthHandling::ZeroIfUnauthorized,
             ctx.j) < amount)
         return tecINSUFFICIENT_FUNDS;
diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
index b70543d280..d5eb80b84d 100644
--- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
@@ -63,6 +63,9 @@ VaultDeposit::preflight(PreflightContext const& ctx)
 TER
 VaultDeposit::preclaim(PreclaimContext const& ctx)
 {
+    auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
+    auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0);
+
     auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
     if (!vault)
         return tecNO_ENTRY;
@@ -107,13 +110,21 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
         // LCOV_EXCL_STOP
     }
 
-    // Cannot deposit inside Vault an Asset frozen for the depositor
-    if (isFrozen(ctx.view, account, vaultAsset))
-        return vaultAsset.holds() ? tecFROZEN : tecLOCKED;
+    if (fix330Enabled)
+    {
+        if (auto const ret = checkDepositFreeze(ctx.view, account, vaultAccount, vaultAsset))
+            return ret;
+    }
+    else
+    {
+        // Cannot deposit inside Vault an Asset frozen for the depositor
+        if (isFrozen(ctx.view, account, vaultAsset))
+            return vaultAsset.holds() ? tecFROZEN : tecLOCKED;
 
-    // Cannot deposit if the shares of the vault are frozen
-    if (isFrozen(ctx.view, account, vaultShare))
-        return tecLOCKED;
+        // Cannot deposit if the shares of the vault are frozen
+        if (isFrozen(ctx.view, account, vaultShare))
+            return tecLOCKED;
+    }
 
     if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner))
     {
@@ -141,7 +152,6 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
     if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter))
         return ter;
 
-    bool const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
     auto const roundedAmount = fix320Enabled ? roundToVaultScale(amount, vault) : amount;
 
     if (fix320Enabled && roundedAmount == beast::kZero)
diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
index 815d8ccd5b..3d30005876 100644
--- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
@@ -65,6 +65,10 @@ VaultWithdraw::preflight(PreflightContext const& ctx)
 TER
 VaultWithdraw::preclaim(PreclaimContext const& ctx)
 {
+    auto const fix313Enabled = ctx.view.rules().enabled(fixCleanup3_1_3);
+    auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
+    auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0);
+
     auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
     if (!vault)
         return tecNO_ENTRY;
@@ -82,8 +86,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
     // lsfMPTCanTransfer flag check, so an issuer cannot trap depositor funds.
     // Other transferability checks (IOU NoRipple, freeze, requireAuth) still
     // apply.
-    auto const waive = ctx.view.rules().enabled(fixCleanup3_2_0) ? WaiveMPTCanTransfer::Yes
-                                                                 : WaiveMPTCanTransfer::No;
+    auto const waive = fix320Enabled ? WaiveMPTCanTransfer::Yes : WaiveMPTCanTransfer::No;
     if (auto ter = canTransfer(ctx.view, vaultAsset, vaultAccount, dstAcct, waive);
         !isTesSuccess(ter))
     {
@@ -100,7 +103,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
         // LCOV_EXCL_STOP
     }
 
-    if (ctx.view.rules().enabled(fixCleanup3_1_3) && amount.asset() == vaultShare)
+    if (fix313Enabled && amount.asset() == vaultShare)
     {
         // Post-fixCleanup3_1_3: if the user specified shares, convert
         // to the equivalent asset amount before checking withdrawal
@@ -160,15 +163,30 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
     if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter))
         return ter;
 
-    // Cannot withdraw from a Vault an Asset frozen for the destination account
-    if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
-        return ret;
-
-    // Cannot return shares to the vault, if the underlying asset was frozen for
-    // the submitter
-    if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
-        return ret;
+    if (fix330Enabled)
+    {
+        // checkWithdrawFreeze checks the underlying asset on the source
+        // (vault pseudo-account), the submitter, and the destination.
+        // A separate share-level freeze check is unnecessary: vault shares
+        // are issued by the vault pseudo-account, which cannot submit
+        // MPTokenIssuanceSet to individually lock a holder's MPToken.
+        // The only way shares become locked is transitively via the
+        // underlying asset, which checkWithdrawFreeze covers.
+        if (auto const ret =
+                checkWithdrawFreeze(ctx.view, vaultAccount, account, dstAcct, vaultAsset))
+            return ret;
+    }
+    else
+    {
+        // Cannot withdraw from a Vault an Asset frozen for the destination account
+        if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
+            return ret;
 
+        // Cannot return shares to the vault, if the underlying asset was frozen for
+        // the submitter
+        if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
+            return ret;
+    }
     return tesSUCCESS;
 }
 
@@ -254,8 +272,14 @@ VaultWithdraw::doApply()
         return tecPATH_DRY;
     }
 
-    if (accountHolds(
-            view(), accountID_, share, FreezeHandling::ZeroIfFrozen, AuthHandling::IgnoreAuth, j_) <
+    // Post-fixCleanup3_3_0: preclaim already validated all freeze conditions
+    // (checkWithdrawFreeze), so IgnoreFreeze avoids a redundant check that
+    // would incorrectly return zero for vault pseudo-accounts whose shares
+    // are frozen via a transitively frozen underlying asset.
+    auto const freezeHandling = view().rules().enabled(fixCleanup3_3_0)
+        ? FreezeHandling::IgnoreFreeze
+        : FreezeHandling::ZeroIfFrozen;
+    if (accountHolds(view(), accountID_, share, freezeHandling, AuthHandling::IgnoreAuth, j_) <
         sharesRedeemed)
     {
         JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough shares";
@@ -358,10 +382,9 @@ VaultWithdraw::doApply()
         // else quietly ignore, account balance is not zero
     }
 
-    auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
-
     associateAsset(*vault, vaultAsset);
 
+    auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
     return doWithdraw(
         view(), ctx_.tx, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
 }
diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp
index 5b576b41e4..9ff6c65c17 100644
--- a/src/test/app/AMMMPT_test.cpp
+++ b/src/test/app/AMMMPT_test.cpp
@@ -18,10 +18,12 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -46,6 +48,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -726,13 +729,14 @@ private:
 
             ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
 
-            if (!features[featureAMMClawback])
+            // Post-fixCleanup3_3_0 a locked holder cannot deposit the other
+            // (non-locked) token either, matching featureAMMClawback.
+            if (!features[featureAMMClawback] && !features[fixCleanup3_3_0])
             {
                 ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt);
             }
             else
             {
-                // Carol can not deposit non-frozen token either
                 ammAlice.deposit(
                     carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
             }
@@ -753,8 +757,15 @@ private:
                 gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
             env.close();
 
-            // Can deposit non-frozen token
-            ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
+            // Post-fixCleanup3_3_0 the deposit checks both pool assets, so the
+            // non-frozen token cannot be deposited while the AMM's USD is frozen.
+            ammAlice.deposit(
+                carol_,
+                btc(100),
+                std::nullopt,
+                std::nullopt,
+                std::nullopt,
+                features[fixCleanup3_3_0] ? Ter(tecFROZEN) : Ter(tesSUCCESS));
 
             // Cannot deposit frozen token
             ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
@@ -773,8 +784,15 @@ private:
             // Individually lock AMM
             btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
 
-            // Can deposit non-frozen token
-            ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt);
+            // Post-fixCleanup3_3_0 the non-locked token cannot be deposited
+            // while the AMM's BTC is locked.
+            ammAlice.deposit(
+                carol_,
+                USD(100),
+                std::nullopt,
+                std::nullopt,
+                std::nullopt,
+                features[fixCleanup3_3_0] ? Ter(tecLOCKED) : Ter(tesSUCCESS));
 
             // Can not deposit locked token
             ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
@@ -788,7 +806,9 @@ private:
             ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
         }
 
-        // Individually lock MPT (AMM) account with MPT/MPT AMM
+        // Individually lock MPT (AMM) account with MPT/MPT AMM.
+        // This block always runs with all amendments (incl. fixCleanup3_3_0),
+        // so the deposit checks both pool assets unconditionally.
         {
             Env env{*this};
             env.fund(XRP(10'000), gw_, alice_, carol_);
@@ -826,8 +846,10 @@ private:
             // Individually lock MPT BTC (AMM) account
             btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
 
-            // Can deposit non-locked token USD
-            ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt);
+            // Post-fixCleanup3_3_0 the non-locked token USD cannot be deposited
+            // while the AMM's BTC is locked.
+            ammAlice.deposit(
+                carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
 
             // Can not deposit locked token BTC
             ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
@@ -843,8 +865,10 @@ private:
             // Individually Lock MPT USD (AMM) account
             usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
 
-            // Can deposit non-locked token BTC
-            ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
+            // Post-fixCleanup3_3_0 the non-locked token BTC cannot be deposited
+            // while the AMM's USD is locked.
+            ammAlice.deposit(
+                carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
 
             // Can not deposit locked token USD
             ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
@@ -876,7 +900,9 @@ private:
             AMM amm(env, alice, XRP(10'000), btc(10'000));
             env.close();
 
-            if (!features[featureAMMClawback])
+            // Post-fixCleanup3_3_0 the deposit requires authorization for both
+            // pool assets, so the unauthorized MPT blocks the XRP deposit too.
+            if (!features[featureAMMClawback] && !features[fixCleanup3_3_0])
             {
                 amm.deposit(carol, XRP(10), std::nullopt, std::nullopt, std::nullopt);
             }
@@ -6863,6 +6889,173 @@ private:
         BEAST_EXPECT(!amm.ammExists());
     }
 
+    void
+    testAMMWithVaultShares()
+    {
+        testcase("AMM with vault shares — underlying freeze blocks share withdrawal");
+        using namespace jtx;
+        // AMMTestBase::testableAmendments() strips featureSingleAssetVault,
+        // but vault shares require it. Use the global jtx set directly.
+        FeatureBitset const all{jtx::testableAmendments()};
+
+        // When alice's underlying asset is individually frozen:
+        //
+        // Deposit (post-fixCleanup3_3_0): checkDepositFreeze checks the AMM
+        //   pseudo-account's underlying, not alice's — deposit is allowed.
+        //   Pre-fix: featureAMMClawback calls isFrozen(alice, share) which
+        //   descends via isVaultPseudoAccountFrozen(alice,...) and finds the
+        //   frozen underlying — deposit is blocked.
+        //
+        // Withdrawal (post-fixCleanup3_3_0): checkWithdrawFreeze ends with
+        //   checkDeepFrozen(alice, share) which calls isFrozen(alice, share)
+        //   and finds the frozen underlying — withdrawal is blocked.
+        //   Pre-fix: the old path only checks the AMM account's MPToken lock,
+        //   which is unset — withdrawal succeeds.
+
+        auto runIOU = [&](FeatureBitset const& features) {
+            bool const fix330 = features[fixCleanup3_3_0];
+            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+
+            env.fund(XRP(100'000), gw_, alice_);
+            env(fset(gw_, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const iou = gw_["IOU"];
+            env.trust(iou(1'000'000), alice_);
+            env(pay(gw_, alice_, iou(10'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou});
+            env(createTx);
+            env.close();
+
+            // 200 IOU → 200,000,000 vault shares (IOU vault scale = 6)
+            env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)}));
+            env.close();
+
+            auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID);
+            // Use half the shares for the AMM; alice keeps the other half.
+            STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000};
+            // Pool: XRP(100) = 1e8 drops, shares = 1e8 → LP ≈ 1e8
+            AMM amm{env, alice_, XRP(100), shareAmt};
+            env.close();
+
+            // Freeze alice's IOU trustline (individual freeze on underlying).
+            env(trust(gw_, iou(0), alice_, tfSetFreeze));
+            env.close();
+
+            // post-fix330: checkDepositFreeze checks AMM pseudo's underlying
+            //              (not alice's) → deposit is allowed
+            // pre-fix330:  featureAMMClawback path calls isFrozen(alice, share)
+            //              which descends to alice's frozen IOU → tecLOCKED
+            amm.deposit(
+                {.account = alice_,
+                 .asset1In = XRP(1),
+                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))});
+
+            // post-fix330: checkWithdrawFreeze → checkDeepFrozen(alice, share)
+            //              descends to alice's frozen IOU → tecLOCKED
+            // pre-fix330:  the AMM pseudo-account is not authorized for the
+            //              share's underlying (requireAuth recurses share→IOU
+            //              and the AMM holds no IOU trustline), so
+            //              accountHolds(ZeroIfUnauthorized) reports the pool's
+            //              share balance as 0 and the withdrawal math fails →
+            //              tecAMM_FAILED.  Vault shares deposited into an AMM are
+            //              only withdrawable once fixCleanup3_3_0 exempts the
+            //              pseudo-account from the recursive auth check.
+            amm.withdraw(
+                {.account = alice_,
+                 .tokens = 1'000,
+                 .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))});
+
+            env(trust(gw_, iou(0), alice_, tfClearFreeze));
+            env.close();
+
+            // Lifting the freeze lets the deposit through in both cases.  The
+            // withdrawal only succeeds post-fix330; pre-fix330 the share balance
+            // remains inaccessible to the unauthorized pseudo-account, so the
+            // shares stay stuck → tecAMM_FAILED.
+            amm.deposit({.account = alice_, .asset1In = XRP(1)});
+            amm.withdraw(
+                {.account = alice_,
+                 .tokens = 1'000,
+                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))});
+        };
+
+        runIOU(all);
+        runIOU(all - fixCleanup3_3_0);
+
+        auto runMPT = [&](FeatureBitset const& features) {
+            bool const fix330 = features[fixCleanup3_3_0];
+            // Expected freeze failures fire invariant checks that log at Error;
+            // silence them so the test output stays clean.
+            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+
+            env.fund(XRP(100'000), gw_, alice_);
+            env.close();
+
+            MPTTester mptt{env, gw_, kMptInitNoFund};
+            mptt.create({.flags = kMptDexFlags | tfMPTCanLock});
+            PrettyAsset const mpt = mptt.issuanceID();
+            mptt.authorize({.account = alice_});
+            env(pay(gw_, alice_, mpt(30'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt});
+            env(createTx);
+            env.close();
+
+            // 20000 MPT → 20000 vault shares (MPT vault scale = 0)
+            env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(20'000)}));
+            env.close();
+
+            auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID);
+            // Use half the shares for the AMM; alice keeps the other half.
+            // Pool: XRP(100) = 1e8 drops, shares = 10000 → LP ≈ 1e6
+            STAmount const shareAmt{MPTIssue{shareMPTID}, 10'000};
+            AMM amm{env, alice_, XRP(100), shareAmt};
+            env.close();
+
+            // Lock alice's underlying MPT (individual lock).
+            mptt.set({.holder = alice_, .flags = tfMPTLock});
+
+            // Same pre/post-fix330 semantics as the IOU case above.
+            amm.deposit(
+                {.account = alice_,
+                 .asset1In = XRP(1),
+                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))});
+
+            // {.tokens = 1'000} → frac = 1000/1e6 = 0.001
+            // XRP out = 1e8 * 0.001 = 1e5 drops, shares out = 10000 * 0.001 = 10
+            // post-fix330: checkWithdrawFreeze sees alice's locked underlying
+            //              MPT via the share → tecLOCKED.
+            // pre-fix330:  the AMM pseudo-account is unauthorized for the
+            //              share's underlying MPT (it holds no underlying
+            //              MPToken), so accountHolds(ZeroIfUnauthorized) zeros
+            //              the pool's share balance and the math fails →
+            //              tecAMM_FAILED.
+            amm.withdraw(
+                {.account = alice_,
+                 .tokens = 1'000,
+                 .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))});
+
+            mptt.set({.holder = alice_, .flags = tfMPTUnlock});
+
+            // Unlocking lets the deposit through; the withdrawal only succeeds
+            // post-fix330 (pre-fix330 the shares remain stuck → tecAMM_FAILED).
+            amm.deposit({.account = alice_, .asset1In = XRP(1)});
+            amm.withdraw(
+                {.account = alice_,
+                 .tokens = 1'000,
+                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))});
+        };
+
+        runMPT(all);
+        runMPT(all - fixCleanup3_3_0);
+    }
+
     void
     testAMMDepositWithFrozenAssets()
     {
@@ -7085,8 +7278,8 @@ private:
         FeatureBitset const all{jtx::testableAmendments()};
         testInstanceCreate();
         testInvalidInstance();
-        testInvalidDeposit(all);
-        testInvalidDeposit(all - featureAMMClawback);
+        for (auto const& f : jtx::amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}, all))
+            testInvalidDeposit(f);
         testDeposit();
         testInvalidWithdraw();
         testWithdraw();
@@ -7113,6 +7306,7 @@ private:
         testLPTokenBalance(all);
         testLPTokenBalance(all - fixAMMv1_3);
         testAMMDepositWithFrozenAssets();
+        testAMMWithVaultShares();
         testAutoDelete();
     }
 };
diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp
index b01d58ddff..3672c4a68a 100644
--- a/src/test/app/AMM_test.cpp
+++ b/src/test/app/AMM_test.cpp
@@ -57,6 +57,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -85,6 +86,13 @@ private:
         return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
     }
 
+    // Seed from the local testableAmendments() which strips SAV and Lending.
+    static std::vector
+    amendmentCombinations(std::initializer_list features)
+    {
+        return jtx::amendmentCombinations(features, testableAmendments());
+    }
+
     void
     testInstanceCreate()
     {
@@ -746,18 +754,19 @@ private:
         testAMM(
             [&](AMM& ammAlice, Env& env) {
                 env(fset(gw_, asfGlobalFreeze));
-                if (!features[featureAMMClawback])
+                auto const freezeBlocksAll =
+                    features[featureAMMClawback] || features[fixCleanup3_3_0];
+                if (!freezeBlocksAll)
                 {
                     // If the issuer set global freeze, the holder still can
-                    // deposit the other non-frozen token when AMMClawback is
-                    // not enabled.
+                    // deposit the other non-frozen token when neither
+                    // AMMClawback nor fixCleanup3_3_0 is enabled.
                     ammAlice.deposit(carol_, XRP(100));
                 }
                 else
                 {
                     // If the issuer set global freeze, the holder cannot
-                    // deposit the other non-frozen token when AMMClawback is
-                    // enabled.
+                    // deposit the other non-frozen token.
                     ammAlice.deposit(
                         carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
                 }
@@ -786,16 +795,18 @@ private:
             [&](AMM& ammAlice, Env& env) {
                 env(trust(gw_, carol_["USD"](0), tfSetFreeze));
                 env.close();
-                if (!features[featureAMMClawback])
+                auto const freezeBlocksAll =
+                    features[featureAMMClawback] || features[fixCleanup3_3_0];
+                if (!freezeBlocksAll)
                 {
-                    // Can deposit non-frozen token if AMMClawback is not
-                    // enabled
+                    // Can deposit non-frozen token if neither AMMClawback
+                    // nor fixCleanup3_3_0 is enabled
                     ammAlice.deposit(carol_, XRP(100));
                 }
                 else
                 {
                     // Cannot deposit non-frozen token if the other token is
-                    // frozen when AMMClawback is enabled
+                    // frozen
                     ammAlice.deposit(
                         carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
                 }
@@ -810,8 +821,18 @@ private:
                     STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
                     tfSetFreeze));
                 env.close();
-                // Can deposit non-frozen token
-                ammAlice.deposit(carol_, XRP(100));
+                // Post-fixCleanup3_3_0: checkDepositFreeze checks both pool
+                // assets against the AMM account, so depositing the
+                // non-frozen token is also blocked.
+                if (!features[fixCleanup3_3_0])
+                {
+                    ammAlice.deposit(carol_, XRP(100));
+                }
+                else
+                {
+                    ammAlice.deposit(
+                        carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
+                }
                 ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
                 ammAlice.deposit(
                     carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
@@ -861,11 +882,10 @@ private:
             AMM amm(env, alice_, XRP(10), gw_["USD"](10), Ter(tesSUCCESS));
             env.close();
 
-            if (features[featureAMMClawback])
+            if (features[featureAMMClawback] || features[fixCleanup3_3_0])
             {
-                // if featureAMMClawback is enabled, bob_ can not deposit XRP
-                // because he's not authorized to hold the paired token
-                // gw_["USD"].
+                // bob_ can not deposit XRP because he's not authorized to
+                // hold the paired token gw_["USD"].
                 amm.deposit(
                     bob_, XRP(10), std::nullopt, std::nullopt, std::nullopt, Ter(tecNO_AUTH));
             }
@@ -1734,36 +1754,57 @@ private:
         });
 
         // Globally frozen asset
-        testAMM([&](AMM& ammAlice, Env& env) {
-            ammAlice.deposit({.account = gw_, .asset1In = USD(1'000), .asset2In = XRP(1'000)});
-            env(fset(gw_, asfGlobalFreeze));
-            env.close();
-            // Can withdraw non-frozen token
-            for (auto const& account : {alice_, gw_})
-            {
-                ammAlice.withdraw(account, XRP(100));
-                ammAlice.withdraw(account, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
-                ammAlice.withdraw(account, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
-            }
-        });
+        testAMM(
+            [&](AMM& ammAlice, Env& env) {
+                auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0);
+                ammAlice.deposit({.account = gw_, .asset1In = USD(1'000), .asset2In = XRP(1'000)});
+                env(fset(gw_, asfGlobalFreeze));
+                env.close();
+                // Can withdraw non-frozen token
+                for (auto const& account : {alice_, gw_})
+                {
+                    ammAlice.withdraw(account, XRP(100));
+                    // Post-fixCleanup3_3_0 the issuer can withdraw their own
+                    // frozen token from the pool.
+                    auto const frozenErr =
+                        (fix330 && account == gw_) ? Ter(tesSUCCESS) : Ter(tecFROZEN);
+                    ammAlice.withdraw(account, USD(100), std::nullopt, std::nullopt, frozenErr);
+                    ammAlice.withdraw(account, 1'000, std::nullopt, std::nullopt, frozenErr);
+                }
+            },
+            std::nullopt,
+            0,
+            std::nullopt,
+            amendmentCombinations({fixCleanup3_3_0}));
 
         // Individually frozen (AMM) account
-        testAMM([&](AMM& ammAlice, Env& env) {
-            env(trust(gw_, alice_["USD"](0), tfSetFreeze));
-            env.close();
-            // Can withdraw non-frozen token
-            ammAlice.withdraw(alice_, XRP(100));
-            ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
-            ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
-            env(trust(gw_, alice_["USD"](0), tfClearFreeze));
-            // Individually frozen AMM
-            env(trust(
-                gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
-            // Can withdraw non-frozen token
-            ammAlice.withdraw(alice_, XRP(100));
-            ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
-            ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
-        });
+        testAMM(
+            [&](AMM& ammAlice, Env& env) {
+                auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0);
+                env(trust(gw_, alice_["USD"](0), tfSetFreeze));
+                env.close();
+                // Can withdraw non-frozen token
+                ammAlice.withdraw(alice_, XRP(100));
+                // Post-fixCleanup3_3_0 regular freeze no longer blocks
+                // self-withdrawal; only deep freeze does.
+                auto const indivFreezeErr = fix330 ? Ter(tesSUCCESS) : Ter(tecFROZEN);
+                ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, indivFreezeErr);
+                ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, indivFreezeErr);
+                env(trust(gw_, alice_["USD"](0), tfClearFreeze));
+                // Individually frozen AMM — still blocked regardless of
+                // fixCleanup3_3_0 because the AMM account itself is frozen.
+                env(trust(
+                    gw_,
+                    STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
+                    tfSetFreeze));
+                ammAlice.withdraw(alice_, XRP(100));
+                ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
+                ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
+            },
+            std::nullopt,
+            0,
+            std::nullopt,
+            amendmentCombinations({fixCleanup3_3_0}));
 
         // Carol withdraws more than she owns
         testAMM([&](AMM& ammAlice, Env&) {
@@ -6659,11 +6700,11 @@ private:
             });
         }
 
-        if (features[featureAMMClawback])
+        if (features[featureAMMClawback] || features[fixCleanup3_3_0])
         {
             // Deposit one asset which is not the frozen token,
-            // but the other asset is frozen. We should get tecFROZEN error
-            // when feature AMMClawback is enabled.
+            // but the other asset is frozen. tecFROZEN when either
+            // AMMClawback or fixCleanup3_3_0 is enabled.
             Env env(*this, features);
             testAMMDeposit(env, [&](AMM& amm) {
                 amm.deposit(
@@ -6673,8 +6714,8 @@ private:
         else
         {
             // Deposit one asset which is not the frozen token,
-            // but the other asset is frozen. We will get tecSUCCESS
-            // when feature AMMClawback is not enabled.
+            // but the other asset is frozen. tesSUCCESS only when
+            // neither AMMClawback nor fixCleanup3_3_0 is enabled.
             Env env(*this, features);
             testAMMDeposit(env, [&](AMM& amm) {
                 amm.deposit(
@@ -7197,8 +7238,8 @@ private:
         FeatureBitset const all{testableAmendments()};
         testInvalidInstance();
         testInstanceCreate();
-        testInvalidDeposit(all);
-        testInvalidDeposit(all - featureAMMClawback);
+        for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}))
+            testInvalidDeposit(f);
         testDeposit();
         testInvalidWithdraw();
         testWithdraw();
@@ -7248,8 +7289,8 @@ private:
         testAMMClawback(all - featureAMMClawback - featureSingleAssetVault);
         testAMMClawback(all - featureAMMClawback);
         testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
-        testAMMDepositWithFrozenAssets(all);
-        testAMMDepositWithFrozenAssets(all - featureAMMClawback);
+        for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}))
+            testAMMDepositWithFrozenAssets(f);
         testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback);
         testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
         testFixReserveCheckOnWithdrawal(all);
diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp
index 804cfaebfc..6aa17c7840 100644
--- a/src/test/app/Invariants_test.cpp
+++ b/src/test/app/Invariants_test.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -4598,6 +4599,210 @@ class Invariants_test : public beast::unit_test::Suite
                 }
             }
         }
+
+        // Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen
+        // on fixCleanup3_3_0.  Pre-amendment, vault-share transfers are allowed
+        // even when the underlying asset is individually frozen for the sender;
+        // post-amendment they are blocked.
+        {
+            Account const gw{"gw"};
+            MPTID shareID{};
+
+            auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                env.fund(XRP(1'000), gw);
+                env.trust(gw["IOU"](10'000), a1);
+                env.trust(gw["IOU"](10'000), a2);
+                env.close();
+                env(pay(gw, a1, gw["IOU"](500)));
+                env(pay(gw, a2, gw["IOU"](500)));
+                env.close();
+
+                PrettyAsset const iou = gw["IOU"];
+                Vault const vault{env};
+                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou});
+                env(createTx);
+                env.close();
+                // Both a1 and a2 deposit IOU, each receiving vault shares.
+                env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)}));
+                env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)}));
+                env.close();
+
+                shareID = env.le(vaultKeylet)->at(sfShareMPTID);
+
+                // Freeze a2's IOU trustline from the issuer side.
+                // a2 is the receiver in the simulated AMM withdraw; the
+                // distinction under test is that pre-fix330 the invariant
+                // does not apply the transitive vault freeze to receivers.
+                env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
+                env.close();
+                return true;
+            };
+
+            // Simulate a vault-share transfer: a1 sends 10 shares to a2.
+            auto const precheck =
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool {
+                auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id()));
+                auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
+                if (!sle1 || !sle2)
+                    return false;
+                (*sle1)[sfMPTAmount] -= 10;
+                (*sle2)[sfMPTAmount] += 10;
+                ac.view().update(sle1);
+                ac.view().update(sle2);
+                return true;
+            };
+
+            // post-fixCleanup3_3_0: full isFrozen() applies to all holders;
+            // isVaultPseudoAccountFrozen finds a2's underlying IOU frozen →
+            // invalidTransfer → invariant fires.
+            doInvariantCheck(
+                Env{*this, defaultAmendments()},
+                {{"invalid MPToken transfer between holders"}},
+                precheck,
+                XRPAmount{},
+                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                preclose);
+
+            // pre-fixCleanup3_3_0: legacy AMM withdraw only checked
+            // checkIndividualFrozen on the destination, not the transitive
+            // vault freeze; a2 as receiver is exempt → invariant passes.
+            doInvariantCheck(
+                Env{*this, defaultAmendments() - fixCleanup3_3_0},
+                {},
+                precheck,
+                XRPAmount{},
+                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
+                {tesSUCCESS, tesSUCCESS},
+                preclose);
+        }
+
+        // Side-specific vault-share AMM_WITHDRAW invariant tests.
+        // Both cases use a real vault (IOU underlying) and a real AMM whose
+        // pool includes vault shares.  precheck simulates an AMM_WITHDRAW by
+        // transferring 10 vault shares from the AMM pseudo-account to a2.
+        {
+            MPTID shareID{};
+            AccountID ammAcctID{};
+            AccountID vaultPseudoID{};
+            Account const gw{"gw"};
+
+            // Simulate AMM_WITHDRAW: AMM pseudo-account sends 10 vault shares
+            // to a2.  The AMM pseudo is the sender (decreasing balance);
+            // a2 is the receiver (increasing balance).
+            auto const precheck2 =
+                [&](Account const& /*a1*/, Account const& a2, ApplyContext& ac) -> bool {
+                auto sleAMM = ac.view().peek(keylet::mptoken(shareID, ammAcctID));
+                auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
+                if (!sleAMM || !sle2)
+                    return false;
+                (*sleAMM)[sfMPTAmount] -= 10;
+                (*sle2)[sfMPTAmount] += 10;
+                ac.view().update(sleAMM);
+                ac.view().update(sle2);
+                return true;
+            };
+
+            // Shared vault + AMM setup: a1 deposits 500 IOU into a vault and
+            // creates an AMM with XRP + 100 vault shares, giving the AMM
+            // pseudo-account a vault-share MPToken balance.
+            auto const setupVaultAMM = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                env.fund(XRP(1'000), gw);
+                env(fset(gw, asfDefaultRipple));
+                env.close();
+
+                env.trust(gw["IOU"](10'000), a1);
+                env.trust(gw["IOU"](10'000), a2);
+                env.close();
+                env(pay(gw, a1, gw["IOU"](1'000)));
+                env(pay(gw, a2, gw["IOU"](500)));
+                env.close();
+
+                Vault const vault{env};
+                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
+                env(createTx);
+                env.close();
+
+                env(vault.deposit(
+                    {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](500)}));
+                env(vault.deposit(
+                    {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](200)}));
+                env.close();
+
+                shareID = env.le(vaultKeylet)->at(sfShareMPTID);
+                vaultPseudoID = env.le(vaultKeylet)->at(sfAccount);
+
+                // a1 creates AMM with XRP + 100 vault shares; the AMM
+                // pseudo-account receives an MPToken record for shareID.
+                AMM const amm(env, a1, XRP(100), STAmount{MPTIssue{shareID}, 100});
+                ammAcctID = amm.ammAccount();
+                return true;
+            };
+
+            // Case 1: freeze the vault pseudo-account's IOU trustline.
+            // isVaultPseudoAccountFrozen(ammAcct) calls isAnyFrozen({vaultPseudo,
+            // ammAcct}, IOU); since vaultPseudo is frozen it returns true.  The
+            // AMM sender has a decreasing balance (not a receiver) so it is
+            // never exempt from the check — invariant fires both pre- and
+            // post-fixCleanup3_3_0.
+            auto const preclose3 = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                if (!setupVaultAMM(a1, a2, env))
+                    return false;
+                env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze));
+                env.close();
+                return true;
+            };
+
+            doInvariantCheck(
+                Env{*this, defaultAmendments()},
+                {{"invalid MPToken transfer between holders"}},
+                precheck2,
+                XRPAmount{},
+                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                preclose3);
+
+            doInvariantCheck(
+                Env{*this, defaultAmendments() - fixCleanup3_3_0},
+                {{"invalid MPToken transfer between holders"}},
+                precheck2,
+                XRPAmount{},
+                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                preclose3);
+
+            // Case 2: freeze a2's (receiver's) IOU trustline.
+            // isVaultPseudoAccountFrozen(a2) → isAnyFrozen({vaultPseudo, a2},
+            // IOU) → true.  The AMM sender's check passes (vaultPseudo and
+            // ammAcct are not frozen).  Pre-fix330: receiver is exempt from
+            // isVaultPseudoAccountFrozen in ttAMM_WITHDRAW → passes.
+            // Post-fix330: full isFrozen() applied to a2 → fires.
+            auto const preclose4 = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                if (!setupVaultAMM(a1, a2, env))
+                    return false;
+                env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
+                env.close();
+                return true;
+            };
+
+            doInvariantCheck(
+                Env{*this, defaultAmendments()},
+                {{"invalid MPToken transfer between holders"}},
+                precheck2,
+                XRPAmount{},
+                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                preclose4);
+
+            doInvariantCheck(
+                Env{*this, defaultAmendments() - fixCleanup3_3_0},
+                {},
+                precheck2,
+                XRPAmount{},
+                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
+                {tesSUCCESS, tesSUCCESS},
+                preclose4);
+        }
     }
 
     void
diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp
index 671f98a901..bdc950eeff 100644
--- a/src/test/app/LoanBroker_test.cpp
+++ b/src/test/app/LoanBroker_test.cpp
@@ -936,11 +936,7 @@ class LoanBroker_test : public beast::unit_test::Suite
             env.close();
             env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)),
                 Ter(tecINSUFFICIENT_FUNDS));
-
-            // preclaim: tecFROZEN
-            env(fset(issuer, asfGlobalFreeze));
-            env.close();
-            env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), Ter(tecFROZEN));
+            // Freeze/lock tests are in testCoverDepositFreezes/testCoverWithdrawFreezes
         }
         else
         {
@@ -966,35 +962,20 @@ class LoanBroker_test : public beast::unit_test::Suite
             // preclaim: tecDST_TAG_NEEDED
             Account const dest{"dest"};
             env.fund(XRP(1'000), dest);
+
             env(fset(dest, asfRequireDest));
-            env.close();
             env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                 kDestination(dest),
                 Ter(tecDST_TAG_NEEDED));
+            env(fclear(dest, asfRequireDest));
 
             // preclaim: tecNO_PERMISSION
-            env(fclear(dest, asfRequireDest));
             env(fset(dest, asfDepositAuth));
-            env.close();
             env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                 kDestination(dest),
                 Ter(tecNO_PERMISSION));
-
-            // preclaim: tecFROZEN
-            env(trust(dest, asset(1'000)));
             env(fclear(dest, asfDepositAuth));
-            env(fset(issuer, asfGlobalFreeze));
-            env.close();
-            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
-                kDestination(dest),
-                Ter(tecFROZEN));
-
-            // preclaim:: tecFROZEN (deep frozen)
-            env(fclear(issuer, asfGlobalFreeze));
-            env(trust(issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze));
-            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
-                kDestination(dest),
-                Ter(tecFROZEN));
+            // Freeze/lock tests are in testCoverDepositFreezes/testCoverWithdrawFreezes
 
             // preclaim: tecPSEUDO_ACCOUNT
             env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
@@ -1784,6 +1765,444 @@ class LoanBroker_test : public beast::unit_test::Suite
         BEAST_EXPECT(aliceBalanceAfter == aliceBalanceBefore);
     }
 
+    void
+    testCoverDepositFreezes()
+    {
+        using namespace jtx;
+        using namespace loanBroker;
+
+        Account const issuer{"issuer"};
+        Account const alice{"alice"};
+
+        // === IOU ===
+        {
+            testcase("LoanBrokerCoverDeposit IOU freeze checks");
+            Env env(*this);
+            Vault const vault{env};
+
+            env.fund(XRP(100'000), issuer, alice);
+            env(trust(alice, issuer["IOU"](1'000'000)));
+            env.close();
+            PrettyAsset const asset(issuer["IOU"]);
+            env(pay(issuer, alice, asset(100'000)));
+            env.close();
+
+            auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
+            env(tx);
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
+            env.close();
+
+            auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+
+            auto const broker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(broker))
+                return;
+            Account const brokerPseudo("pseudo", broker->at(sfAccount));
+
+            env(coverDeposit(alice, brokerKeylet.key, asset(10)));
+            env.close();
+
+            auto runTests = [&]() {
+                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+                // Global freeze
+                env(fset(issuer, asfGlobalFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
+                env(fclear(issuer, asfGlobalFreeze));
+
+                // Source regular freeze
+                env(trust(issuer, asset(0), alice, tfSetFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
+                env(trust(issuer, asset(0), alice, tfClearFreeze));
+
+                // Source deep freeze
+                env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
+                env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));
+
+                // Pseudo regular freeze — post-fix blocks, pre-fix allows (BUG)
+                TER const pseudoTer = fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS);
+                env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(pseudoTer));
+                env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze));
+
+                // Pseudo deep freeze
+                env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze | tfSetDeepFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
+                env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze | tfClearDeepFreeze));
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+
+        // === MPT ===
+        {
+            testcase("LoanBrokerCoverDeposit MPT lock checks");
+            Env env(*this);
+            Vault const vault{env};
+
+            env.fund(XRP(100'000), issuer, alice);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const mpt{mptt.issuanceID()};
+
+            mptt.authorize({.account = alice});
+            env(pay(issuer, alice, mpt(100'000)));
+            env.close();
+
+            auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
+            env(tx);
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)}));
+            env.close();
+
+            auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+
+            auto const broker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(broker))
+                return;
+            Account const brokerPseudo("pseudo", broker->at(sfAccount));
+
+            env(coverDeposit(alice, brokerKeylet.key, mpt(10)));
+            env.close();
+
+            // For MPT isDeepFrozen == isFrozen, so all locks block in
+            // both pre- and post-fix. No behavioral difference.
+            auto runTests = [&]() {
+                // Global lock
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+
+                // Source (alice) individual lock
+                mptt.set({.holder = alice, .flags = tfMPTLock});
+                env.close();
+                env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
+                mptt.set({.holder = alice, .flags = tfMPTUnlock});
+                env.close();
+
+                // Pseudo individual lock
+                mptt.set({.holder = brokerPseudo, .flags = tfMPTLock});
+                env.close();
+                env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
+                mptt.set({.holder = brokerPseudo, .flags = tfMPTUnlock});
+                env.close();
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+    }
+
+    // Focused demonstration: a cover-withdraw submitter under a regular
+    // individual IOU freeze can still withdraw to themselves (self-withdrawal).
+    //
+    // Pre-fixCleanup3_3_0: the old code only checked the pseudo-account source
+    // and the destination for deep-freeze; it did not check the submitter's
+    // individual freeze at all. Self-withdrawal therefore always succeeded.
+    // Post-fixCleanup3_3_0: checkWithdrawFreeze explicitly skips the submitter
+    // freeze check when submitter == destination, preserving the same result.
+    void
+    testCoverWithdrawSelfWhileFrozen()
+    {
+        testcase("LoanBrokerCoverWithdraw IOU self-withdrawal while individually frozen");
+
+        using namespace jtx;
+        using namespace loanBroker;
+
+        Account const issuer{"issuer"};
+        Account const alice{"alice"};
+        Account const dest{"dest"};
+        Env env{*this};
+        Vault const vault{env};
+
+        env.fund(XRP(100'000), issuer, alice, dest);
+        env(trust(alice, issuer["IOU"](1'000'000)));
+        env(trust(dest, issuer["IOU"](1'000'000)));
+        env.close();
+
+        PrettyAsset const asset(issuer["IOU"]);
+        env(pay(issuer, alice, asset(100'000)));
+        env.close();
+
+        auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
+        env(vaultTx);
+        env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
+        env.close();
+
+        auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
+        env(set(alice, vaultKeylet.key));
+        env.close();
+
+        env(coverDeposit(alice, brokerKeylet.key, asset(10)));
+        env.close();
+
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+            // Set a regular individual freeze on alice's IOU trustline.
+            env(trust(issuer, asset(0), alice, tfSetFreeze));
+            env.close();
+
+            // Self-withdrawal: submitter == destination (no sfDestination in tx).
+            // Both pre- and post-fixCleanup3_3_0 this succeeds:
+            //   pre-fix:  old code never checked the submitter's freeze.
+            //   post-fix: checkWithdrawFreeze skips submitter when submitter==dst.
+            env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tesSUCCESS));
+
+            // Withdrawal to a third party is blocked by the submitter freeze
+            // under fixCleanup3_3_0; pre-fix it was not checked.
+            env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                kDestination(dest),
+                Ter(fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS)));
+
+            env(trust(issuer, asset(0), alice, tfClearFreeze));
+            env.close();
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+    void
+    testCoverWithdrawFreezes()
+    {
+        using namespace jtx;
+        using namespace loanBroker;
+
+        Account const issuer{"issuer"};
+        Account const alice{"alice"};
+
+        // === IOU ===
+        {
+            testcase("LoanBrokerCoverWithdraw IOU freeze checks");
+            Env env(*this);
+            Vault const vault{env};
+
+            env.fund(XRP(100'000), issuer, alice);
+            env(trust(alice, issuer["IOU"](1'000'000)));
+            env.close();
+            PrettyAsset const asset(issuer["IOU"]);
+            env(pay(issuer, alice, asset(100'000)));
+            env.close();
+
+            auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
+            env(tx);
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
+            env.close();
+
+            auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+
+            auto const broker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(broker))
+                return;
+            Account const brokerPseudo("pseudo", broker->at(sfAccount));
+
+            env(coverDeposit(alice, brokerKeylet.key, asset(10)));
+            env.close();
+
+            Account const dest{"dest"};
+            env.fund(XRP(1'000), dest);
+            env(trust(dest, asset(1'000)));
+
+            auto runTests = [&]() {
+                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+                TER const expectedTec = fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS);
+
+                // Global freeze
+                env(fset(issuer, asfGlobalFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(tecFROZEN));
+                env(fclear(issuer, asfGlobalFreeze));
+
+                // Source (pseudo) regular freeze
+                env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze));
+
+                // Source (pseudo) deep freeze
+                env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze | tfSetDeepFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze | tfClearDeepFreeze));
+
+                // Submitter regular freeze → dest
+                env(trust(issuer, asset(0), alice, tfSetFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(expectedTec));
+                // Submitter regular freeze → self: always allowed
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tesSUCCESS));
+                env(trust(issuer, asset(0), alice, tfClearFreeze));
+                env(coverDeposit(
+                    alice, brokerKeylet.key, asset(isTesSuccess(expectedTec) ? 2 : 1)));
+
+                // Submitter deep freeze → dest
+                env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(expectedTec));
+                // Submitter deep freeze → self: blocked (checkDeepFrozen)
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
+                env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));
+                if (isTesSuccess(expectedTec))
+                    env(coverDeposit(alice, brokerKeylet.key, asset(1)));
+
+                // Destination regular freeze: only deep freeze blocks
+                env(trust(issuer, asset(0), dest, tfSetFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(tesSUCCESS));
+                env(trust(issuer, asset(0), dest, tfClearFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)));
+
+                // Destination deep freeze
+                env(trust(issuer, asset(0), dest, tfSetFreeze | tfSetDeepFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(dest),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), dest, tfClearFreeze | tfClearDeepFreeze));
+
+                // Submitter frozen → issuer: bypasses all freeze checks
+                env(trust(issuer, asset(0), alice, tfSetFreeze));
+                env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
+                    kDestination(issuer),
+                    Ter(tesSUCCESS));
+                env(trust(issuer, asset(0), alice, tfClearFreeze));
+                env(coverDeposit(alice, brokerKeylet.key, asset(1)));
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+
+        // === MPT ===
+        {
+            testcase("LoanBrokerCoverWithdraw MPT lock checks");
+            Env env(*this);
+            Vault const vault{env};
+
+            env.fund(XRP(100'000), issuer, alice);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const mpt{mptt.issuanceID()};
+
+            mptt.authorize({.account = alice});
+            env(pay(issuer, alice, mpt(100'000)));
+            env.close();
+
+            auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
+            env(tx);
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)}));
+            env.close();
+
+            auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+
+            auto const broker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(broker))
+                return;
+            Account const brokerPseudo("pseudo", broker->at(sfAccount));
+
+            env(coverDeposit(alice, brokerKeylet.key, mpt(10)));
+            env.close();
+
+            Account const dest{"dest"};
+            env.fund(XRP(1'000), dest);
+            mptt.authorize({.account = dest});
+            env.close();
+
+            auto runTests = [&]() {
+                auto const withFix = env.current()->rules().enabled(fixCleanup3_3_0);
+                // Only submitter-to-dest differs: post-fix blocks, pre-fix
+                // doesn't (BUG). All other locks block in both because for
+                // MPT isDeepFrozen == isFrozen.
+                TER const submitterToDest = withFix ? TER(tecLOCKED) : TER(tesSUCCESS);
+
+                // Global lock
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
+                    kDestination(dest),
+                    Ter(tecLOCKED));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+
+                // Source (pseudo) individual lock
+                mptt.set({.holder = brokerPseudo, .flags = tfMPTLock});
+                env.close();
+                env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
+                    kDestination(dest),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = brokerPseudo, .flags = tfMPTUnlock});
+                env.close();
+
+                // Submitter individual lock → dest
+                mptt.set({.holder = alice, .flags = tfMPTLock});
+                env.close();
+                env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
+                    kDestination(dest),
+                    Ter(submitterToDest));
+                // Submitter individual lock → self: blocked
+                env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
+                mptt.set({.holder = alice, .flags = tfMPTUnlock});
+                env.close();
+                if (isTesSuccess(submitterToDest))
+                    env(coverDeposit(alice, brokerKeylet.key, mpt(1)));
+                env.close();
+
+                // Dest individual lock: blocked
+                mptt.set({.holder = dest, .flags = tfMPTLock});
+                env.close();
+                env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
+                    kDestination(dest),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = dest, .flags = tfMPTUnlock});
+                env.close();
+
+                // Submitter locked → issuer: bypasses all freeze checks
+                mptt.set({.holder = alice, .flags = tfMPTLock});
+                env.close();
+                env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
+                    kDestination(issuer),
+                    Ter(tesSUCCESS));
+                mptt.set({.holder = alice, .flags = tfMPTUnlock});
+                env(coverDeposit(alice, brokerKeylet.key, mpt(1)));
+                env.close();
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+    }
+
     void
     testRIPD4274IOU()
     {
@@ -2244,6 +2663,13 @@ public:
     void
     run() override
     {
+        testInvalidLoanBrokerCoverClawback();
+        testInvalidLoanBrokerCoverDeposit();
+        testInvalidLoanBrokerCoverWithdraw();
+        testCoverDepositFreezes();
+        testCoverWithdrawFreezes();
+        testCoverWithdrawSelfWhileFrozen();
+
         testCoverPrecisionGuard();
 
         testLoanBrokerSetDebtMaximum();
@@ -2251,9 +2677,6 @@ public:
 
         testDisabled();
         testLifecycle();
-        testInvalidLoanBrokerCoverClawback();
-        testInvalidLoanBrokerCoverDeposit();
-        testInvalidLoanBrokerCoverWithdraw();
         testInvalidLoanBrokerDelete();
         testInvalidLoanBrokerSet();
         testRequireAuth();
@@ -2268,7 +2691,6 @@ public:
 
         testLoanBrokerDeleteFrozenIOU(all_);
         testLoanBrokerDeleteFrozenIOU(all_ - fixCleanup3_2_0);
-
         // TODO: Write clawback failure tests with an issuer / MPT that doesn't
         // have the right flags set.
     }
diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp
index 1b0dd414e1..56a7cab47a 100644
--- a/src/test/app/Loan_test.cpp
+++ b/src/test/app/Loan_test.cpp
@@ -92,23 +92,6 @@ protected:
     // even if they are set to unsupported.
 
     FeatureBitset const all_{jtx::testableAmendments()};
-
-    // All 2^N permutations of `all_` with each subset of the given features
-    // excluded. The first entry is always `all_` itself (empty exclusion);
-    // the last excludes every feature in the list.
-    std::vector
-    amendmentCombinations(std::initializer_list features) const
-    {
-        std::vector result{all_};
-        for (auto const& f : features)
-        {
-            auto const n = result.size();
-            for (std::size_t i = 0; i < n; ++i)
-                result.push_back(result[i] - f);
-        }
-        return result;
-    }
-
     std::string const iouCurrency_{"IOU"};
 
     void
@@ -8624,8 +8607,8 @@ public:
     run() override
     {
         runAmendmentIndependent();
-        for (auto const& features :
-             amendmentCombinations({fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}))
+        for (auto const& features : jtx::amendmentCombinations(
+                 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
             runAmendmentSensitive(features);
     }
 };
diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp
index bfb80d3e36..323184aa36 100644
--- a/src/test/app/MPToken_test.cpp
+++ b/src/test/app/MPToken_test.cpp
@@ -7461,21 +7461,42 @@ class MPToken_test : public beast::unit_test::Suite
 
                 // MPTLock is set
                 usd.set({.flags = tfMPTLock});
-                // carol and issuer can't withdraw
-                for (auto const& account : {carol, gw})
-                {
-                    amm.withdraw(
-                        {.account = account,
-                         .asset1Out = usd(1),
-                         .asset2Out = eur(1),
-                         .err = Ter(tecLOCKED)});
-                    amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecLOCKED)});
-                    // can single withdraw another asset
-                    amm.withdraw(
-                        {.account = account,
-                         .asset1Out = eur(1),
-                         .assets = std::make_pair(eur, usd)});
-                }
+                auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0);
+
+                // carol can't withdraw the locked token (any withdrawal type)
+                amm.withdraw(
+                    {.account = carol,
+                     .asset1Out = usd(1),
+                     .asset2Out = eur(1),
+                     .err = Ter(tecLOCKED)});
+                amm.withdraw({.account = carol, .tokens = 1'000, .err = Ter(tecLOCKED)});
+                // can single withdraw the non-locked asset
+                amm.withdraw(
+                    {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
+
+                // post-fixCleanup3_3_0 the issuer can redeem even when locked.
+                // Each successful withdrawal burns LP tokens, so replenish between
+                // each type to keep gw's LP balance stable.
+                auto const gwLockErr = fix330 ? Ter(tesSUCCESS) : Ter(tecLOCKED);
+                auto const replenish = [&](IOUAmount tokens) {
+                    usd.set({.flags = tfMPTUnlock});
+                    amm.deposit({.account = gw, .tokens = tokens});
+                    usd.set({.flags = tfMPTLock});
+                };
+
+                amm.withdraw(
+                    {.account = gw, .asset1Out = usd(1), .asset2Out = eur(1), .err = gwLockErr});
+                if (fix330)
+                    replenish(IOUAmount{1});
+
+                amm.withdraw({.account = gw, .tokens = 1'000, .err = gwLockErr});
+                if (fix330)
+                    replenish(IOUAmount{1'000});
+
+                // can single withdraw the non-locked asset
+                amm.withdraw(
+                    {.account = gw, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
+
                 usd.set({.flags = tfMPTUnlock});
 
                 // MPTRequireAuth is set
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index 4643bc8555..6fefcfc404 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -1573,6 +1573,7 @@ class Vault_test : public beast::unit_test::Suite
             bool enableClawback = true;
             bool requireAuth = true;
             int initialXRP = 1000;
+            FeatureBitset features = testableAmendments();
         };
 
         auto testCase = [this](
@@ -1585,7 +1586,7 @@ class Vault_test : public beast::unit_test::Suite
                                 Vault& vault,
                                 MPTTester& mptt)> test,
                             CaseArgs args = {}) {
-            Env env{*this, testableAmendments()};
+            Env env{*this, args.features};
             Account const issuer{"issuer"};
             Account const owner{"owner"};
             Account const depositor{"depositor"};
@@ -1632,6 +1633,8 @@ class Vault_test : public beast::unit_test::Suite
             env(tx, Ter(tecNO_ENTRY));
         });
 
+        // Freeze/lock tests are in testVaultDepositFreeze/testVaultWithdrawFreeze
+
         testCase([this](
                      Env& env,
                      Account const& issuer,
@@ -1646,81 +1649,6 @@ class Vault_test : public beast::unit_test::Suite
             env(tx, Ter(tecLOCKED));
         });
 
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT global lock blocks deposit");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            mptt.set({.account = issuer, .flags = tfMPTLock});
-            env.close();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx, Ter{tecLOCKED});
-            env.close();
-
-            // Can delete empty vault, even if global lock
-            tx = vault.del({.owner = owner, .id = keylet.key});
-            env(tx);
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT global lock blocks withdrawal");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx);
-            env.close();
-
-            // Check that the OutstandingAmount field of MPTIssuance
-            // accounts for the issued shares.
-            auto v = env.le(keylet);
-            BEAST_EXPECT(v);
-            MPTID const share = (*v)[sfShareMPTID];
-            auto issuance = env.le(keylet::mptokenIssuance(share));
-            BEAST_EXPECT(issuance);
-            Number const outstandingShares = issuance->at(sfOutstandingAmount);
-            BEAST_EXPECT(outstandingShares == 100);
-
-            mptt.set({.account = issuer, .flags = tfMPTLock});
-            env.close();
-
-            tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx, Ter(tecLOCKED));
-
-            tx[sfDestination] = issuer.human();
-            env(tx, Ter(tecLOCKED));
-
-            // Clawback is still permitted, even with global lock
-            tx = vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
-            env(tx);
-            env.close();
-
-            // Clawback removed shares MPToken
-            auto const mptSle = env.le(keylet::mptoken(share, depositor.id()));
-            BEAST_EXPECT(mptSle == nullptr);
-
-            // Can delete empty vault, even if global lock
-            tx = vault.del({.owner = owner, .id = keylet.key});
-            env(tx);
-        });
-
         testCase([this](
                      Env& env,
                      Account const& issuer,
@@ -2155,57 +2083,6 @@ class Vault_test : public beast::unit_test::Suite
             env(vault.del({.owner = owner, .id = keylet.key}));
         });
 
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT lock of vault pseudo-account");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            auto const vaultAccount = [&env, keylet = keylet, this]() -> AccountID {
-                auto const vault = env.le(keylet);
-                BEAST_EXPECT(vault != nullptr);
-                return vault->at(sfAccount);
-            }();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx);
-            env.close();
-
-            tx = [&]() {
-                json::Value jv;
-                jv[jss::Account] = issuer.human();
-                jv[sfMPTokenIssuanceID] = to_string(asset.get().getMptID());
-                jv[jss::Holder] = toBase58(vaultAccount);
-                jv[jss::TransactionType] = jss::MPTokenIssuanceSet;
-                jv[jss::Flags] = tfMPTLock;
-                return jv;
-            }();
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx, Ter(tecLOCKED));
-
-            tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx, Ter(tecLOCKED));
-
-            // Clawback works, even when locked
-            tx = vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(100)});
-            env(tx);
-
-            // Can delete an empty vault even when asset is locked.
-            tx = vault.del({.owner = owner, .id = keylet.key});
-            env(tx);
-        });
-
         {
             testcase("MPT shares to a vault");
 
@@ -2438,6 +2315,7 @@ class Vault_test : public beast::unit_test::Suite
             Number initialIOU = 200;
             double transferRate = 1.0;
             bool charlieRipple = true;
+            FeatureBitset features = testableAmendments();
         };
 
         auto testCase = [&, this](
@@ -2451,7 +2329,7 @@ class Vault_test : public beast::unit_test::Suite
                                 PrettyAsset const& asset,
                                 std::function issuanceId)> test,
                             CaseArgs args = {}) {
-            Env env{*this, testableAmendments()};
+            Env env{*this, args.features};
             Account const owner{"owner"};
             Account const issuer{"issuer"};
             Account const charlie{"charlie"};
@@ -2543,83 +2421,6 @@ class Vault_test : public beast::unit_test::Suite
             env.close();
         });
 
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const& charlie,
-                     auto vaultAccount,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto issuanceId) {
-            testcase("IOU frozen trust line to vault account");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            Asset const share = Asset(issuanceId(keylet));
-
-            // Freeze the trustline to the vault
-            auto trustSet = [&, account = vaultAccount(keylet)]() {
-                json::Value jv;
-                jv[jss::Account] = issuer.human();
-                {
-                    auto& ja = jv[jss::LimitAmount] =
-                        asset(0).value().getJson(JsonOptions::Values::None);
-                    ja[jss::issuer] = toBase58(account);
-                }
-                jv[jss::TransactionType] = jss::TrustSet;
-                jv[jss::Flags] = tfSetFreeze;
-                return jv;
-            }();
-            env(trustSet);
-            env.close();
-
-            {
-                // Note, the "frozen" state of the trust line to vault account
-                // is reported as  "locked" state of the vault shares, because
-                // this state is attached to shares by means of the transitive
-                // isFrozen.
-                auto tx =
-                    vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(80)});
-                env(tx, Ter{tecLOCKED});
-            }
-
-            {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)});
-                env(tx, Ter{tecLOCKED});
-
-                // also when trying to withdraw to a 3rd party
-                tx[sfDestination] = charlie.human();
-                env(tx, Ter{tecLOCKED});
-                env.close();
-            }
-
-            {
-                // Clawback works, even when locked
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
-                env(tx);
-                env.close();
-            }
-
-            // Clear the frozen state
-            trustSet[jss::Flags] = tfClearFreeze;
-            env(trustSet);
-            env.close();
-
-            env(vault.withdraw(
-                {.depositor = owner, .id = keylet.key, .amount = share(50'000'000)}));
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-            env.close();
-        });
-
         testCase(
             [&, this](
                 Env& env,
@@ -2681,65 +2482,6 @@ class Vault_test : public beast::unit_test::Suite
             },
             CaseArgs{.transferRate = 1.25});
 
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const& charlie,
-                     auto,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto&&...) {
-            testcase("IOU frozen trust line to depositor");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            // Withdraw to 3rd party works
-            auto const withdrawToCharlie = [&](xrpl::Keylet keylet) {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[sfDestination] = charlie.human();
-                return tx;
-            }(keylet);
-            env(withdrawToCharlie);
-
-            // Freeze the owner
-            env(trust(issuer, asset(0), owner, tfSetFreeze));
-            env.close();
-
-            // Cannot withdraw
-            auto const withdraw =
-                vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-            env(withdraw, Ter{tecFROZEN});
-
-            // Cannot withdraw to 3rd party
-            env(withdrawToCharlie, Ter{tecLOCKED});
-            env.close();
-
-            {
-                // Cannot deposit some more
-                auto tx =
-                    vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                env(tx, Ter{tecFROZEN});
-            }
-
-            {
-                // Clawback still works
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
-                env(tx);
-                env.close();
-            }
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-            env.close();
-        });
-
         testCase([&, this](
                      Env& env,
                      Account const& owner,
@@ -3031,102 +2773,6 @@ class Vault_test : public beast::unit_test::Suite
                 env.close();
             },
             CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
-
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const& charlie,
-                     auto,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto&&...) {
-            testcase("IOU frozen trust line to 3rd party");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            // Withdraw to 3rd party works
-            auto const withdrawToCharlie = [&](xrpl::Keylet keylet) {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[sfDestination] = charlie.human();
-                return tx;
-            }(keylet);
-            env(withdrawToCharlie);
-
-            // Freeze the 3rd party
-            env(trust(issuer, asset(0), charlie, tfSetFreeze));
-            env.close();
-
-            // Can withdraw
-            auto const withdraw =
-                vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-            env(withdraw);
-            env.close();
-
-            // Cannot withdraw to 3rd party
-            env(withdrawToCharlie, Ter{tecFROZEN});
-            env.close();
-
-            env(vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}));
-            env.close();
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-            env.close();
-        });
-
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const& charlie,
-                     auto,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto&&...) {
-            testcase("IOU global freeze");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            env(fset(issuer, asfGlobalFreeze));
-            env.close();
-
-            {
-                // Cannot withdraw
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                env(tx, Ter{tecFROZEN});
-
-                // Cannot withdraw to 3rd party
-                tx[sfDestination] = charlie.human();
-                env(tx, Ter{tecFROZEN});
-                env.close();
-
-                // Cannot deposit some more
-                tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-
-                env(tx, Ter{tecFROZEN});
-            }
-
-            // Clawback is permitted
-            env(vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}));
-            env.close();
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-            env.close();
-        });
     }
 
     void
@@ -7824,6 +7470,579 @@ class Vault_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testVaultDepositFreeze()
+    {
+        using namespace test::jtx;
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+
+        // === IOU ===
+        {
+            testcase("VaultDeposit IOU freeze checks");
+            Env env{*this};
+            Vault vault{env};
+
+            env.fund(XRP(100'000), issuer, owner);
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1'000'000), owner);
+            env(pay(issuer, owner, asset(100'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+            auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
+
+            // Initial deposit so the vault pseudo-account has a trustline
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+            env.close();
+
+            auto runTests = [&]() {
+                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+                // Global freeze
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                env(fclear(issuer, asfGlobalFreeze));
+
+                // Depositor regular freeze
+                env(trust(issuer, asset(0), owner, tfSetFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), owner, tfClearFreeze));
+
+                // Depositor deep freeze
+                env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
+
+                // Vault-account regular freeze
+                // Post-fix: checkDepositFreeze catches it → tecFROZEN
+                // Pre-fix: not checked directly, but the transitive share
+                //          check triggers → tecLOCKED
+                {
+                    auto trustSet = [&]() {
+                        json::Value jv;
+                        jv[jss::Account] = issuer.human();
+                        {
+                            auto& ja = jv[jss::LimitAmount] =
+                                asset(0).value().getJson(JsonOptions::Values::None);
+                            ja[jss::issuer] = toBase58(vaultAcct.id());
+                        }
+                        jv[jss::TransactionType] = jss::TrustSet;
+                        return jv;
+                    }();
+
+                    trustSet[jss::Flags] = tfSetFreeze;
+                    env(trustSet);
+                    env.close();
+
+                    TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                        Ter(expected));
+
+                    trustSet[jss::Flags] = tfClearFreeze;
+                    env(trustSet);
+                    env.close();
+                }
+
+                // Vault-account deep freeze
+                {
+                    auto trustSet = [&]() {
+                        json::Value jv;
+                        jv[jss::Account] = issuer.human();
+                        {
+                            auto& ja = jv[jss::LimitAmount] =
+                                asset(0).value().getJson(JsonOptions::Values::None);
+                            ja[jss::issuer] = toBase58(vaultAcct.id());
+                        }
+                        jv[jss::TransactionType] = jss::TrustSet;
+                        return jv;
+                    }();
+
+                    trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze;
+                    env(trustSet);
+                    env.close();
+
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                        Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+
+                    trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze;
+                    env(trustSet);
+                    env.close();
+                }
+
+                // Clawback works while frozen
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
+                env(fclear(issuer, asfGlobalFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env.close();
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+
+        // === MPT ===
+        {
+            testcase("VaultDeposit MPT lock checks");
+            Env env{*this};
+            Vault vault{env};
+
+            env.fund(XRP(100'000), issuer, owner);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create(
+                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+            PrettyAsset const mpt{mptt.issuanceID()};
+
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = issuer, .holder = owner});
+            env.close();
+            env(pay(issuer, owner, mpt(100'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
+            env(tx);
+            env.close();
+            auto const vaultAcctID = env.le(keylet)->at(sfAccount);
+            Account const vaultAcct("vault", vaultAcctID);
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
+            env.close();
+
+            // For MPT isDeepFrozen == isFrozen, so all locks block in
+            // both pre- and post-fix.
+            auto runTests = [&]() {
+                // Global lock
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+
+                // Depositor individual lock
+                mptt.set({.holder = owner, .flags = tfMPTLock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = owner, .flags = tfMPTUnlock});
+                env.close();
+
+                // Vault pseudo-account individual lock
+                mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
+                env.close();
+
+                // Clawback works while locked
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                env.close();
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+    }
+
+    // Focused demonstration: a depositor under a regular individual IOU freeze
+    // can still withdraw to themselves (self-withdrawal), but is blocked from
+    // withdrawing to a third party.
+    //
+    // Pre-fixCleanup3_3_0: both the self-withdrawal AND the third-party
+    // withdrawal were blocked because the old code checked checkFrozen on the
+    // destination regardless of whether it was the submitter.
+    // Post-fixCleanup3_3_0: checkWithdrawFreeze skips the submitter freeze
+    // check when submitter == destination, so self-withdrawal succeeds.
+    void
+    testVaultSelfWithdrawWhileFrozen()
+    {
+        testcase("VaultWithdraw IOU self-withdrawal while individually frozen");
+
+        using namespace test::jtx;
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const charlie{"charlie"};
+        Env env{*this};
+        Vault vault{env};
+
+        env.fund(XRP(100'000), issuer, owner, charlie);
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1'000'000), owner);
+        env.trust(asset(1'000'000), charlie);
+        env(pay(issuer, owner, asset(100'000)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)}));
+        env.close();
+
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+            // Set a regular individual freeze on the owner's IOU trustline.
+            env(trust(issuer, asset(0), owner, tfSetFreeze));
+            env.close();
+
+            // Self-withdrawal: submitter == destination, so the submitter
+            // freeze check is skipped.
+            // Post-fix: tesSUCCESS.  Pre-fix: tecFROZEN.
+            env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+
+            // Withdrawal to a third party is blocked: submitter != destination
+            // so the submitter freeze check applies.
+            {
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                // Post-fix: tecFROZEN (checkIndividualFrozen on submitter).
+                // Pre-fix: tecLOCKED (isFrozen on the vault share).
+                env(withdrawToCharlie, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+            }
+
+            env(trust(issuer, asset(0), owner, tfClearFreeze));
+            env.close();
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+    void
+    testVaultWithdrawFreeze()
+    {
+        using namespace test::jtx;
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+
+        // === IOU ===
+        {
+            testcase("VaultWithdraw IOU freeze checks");
+            Env env{*this};
+            Vault vault{env};
+
+            env.fund(XRP(100'000), issuer, owner);
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1'000'000), owner);
+            env(pay(issuer, owner, asset(100'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+            auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+            env.close();
+
+            Account const charlie{"charlie"};
+            env.fund(XRP(10'000), charlie);
+            env.trust(asset(1'000'000), charlie);
+            env.close();
+
+            auto runTests = [&]() {
+                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+                // Post-fix: submitter freeze blocks withdraw to 3rd party
+                // Pre-fix: submitter's IOU freeze not checked, but
+                //          checkFrozen(depositor, share) may trigger tecLOCKED
+                TER const submitterTo3rd = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+
+                // Global freeze → self-withdraw
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                // Global freeze → withdraw to 3rd party
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    env(withdrawToCharlie, Ter(tecFROZEN));
+                }
+                env(fclear(issuer, asfGlobalFreeze));
+
+                // Vault-account regular freeze
+                {
+                    auto trustSet = [&]() {
+                        json::Value jv;
+                        jv[jss::Account] = issuer.human();
+                        {
+                            auto& ja = jv[jss::LimitAmount] =
+                                asset(0).value().getJson(JsonOptions::Values::None);
+                            ja[jss::issuer] = toBase58(vaultAcct.id());
+                        }
+                        jv[jss::TransactionType] = jss::TrustSet;
+                        return jv;
+                    }();
+
+                    trustSet[jss::Flags] = tfSetFreeze;
+                    env(trustSet);
+                    env.close();
+
+                    TER const vaultAcctFreeze = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+
+                    // Self-withdraw
+                    env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                        Ter(vaultAcctFreeze));
+                    // Withdraw to 3rd party
+                    {
+                        auto withdrawToCharlie = vault.withdraw(
+                            {.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                        withdrawToCharlie[sfDestination] = charlie.human();
+                        env(withdrawToCharlie, Ter(vaultAcctFreeze));
+                    }
+
+                    trustSet[jss::Flags] = tfClearFreeze;
+                    env(trustSet);
+                    env.close();
+                }
+
+                // Depositor regular freeze → self-withdraw
+                env(trust(issuer, asset(0), owner, tfSetFreeze));
+                // Post-fix: self-withdraw allowed (submitter==dst skip)
+                // Pre-fix: isFrozen(depositor, iou) catches it
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+
+                // Depositor regular freeze → withdraw to 3rd party
+                {
+                    auto withdrawTo3rd =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                    withdrawTo3rd[sfDestination] = charlie.human();
+                    env(withdrawTo3rd, Ter(submitterTo3rd));
+                }
+                env(trust(issuer, asset(0), owner, tfClearFreeze));
+                // Replenish what was withdrawn
+                if (fix330Enabled)
+                {
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                }
+                env.close();
+
+                // Depositor deep freeze → self-withdraw blocked
+                env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN)));
+                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
+
+                // Destination regular freeze → withdraw to 3rd party
+                env(trust(issuer, asset(0), charlie, tfSetFreeze));
+                // Self-withdraw unaffected by charlie's freeze
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    // Post-fix: regular freeze on dst allowed
+                    // Pre-fix: checkFrozen(dst, iou) catches it
+                    env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+                }
+                env(trust(issuer, asset(0), charlie, tfClearFreeze));
+                // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded
+                env(vault.deposit(
+                    {.depositor = owner,
+                     .id = keylet.key,
+                     .amount = asset(fix330Enabled ? 2 : 1)}));
+                env.close();
+
+                // Destination deep freeze → withdraw to 3rd party blocked
+                env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze));
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    env(withdrawToCharlie, Ter(tecFROZEN));
+                }
+                // Destination deep freeze → self-withdraw unaffected
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env.close();
+
+                // Clawback works while frozen
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
+                env(fclear(issuer, asfGlobalFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env.close();
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+
+        // === MPT ===
+        {
+            testcase("VaultWithdraw MPT lock checks");
+            Env env{*this};
+            Vault vault{env};
+
+            env.fund(XRP(100'000), issuer, owner);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create(
+                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+            PrettyAsset const mpt{mptt.issuanceID()};
+
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = issuer, .holder = owner});
+            env.close();
+            env(pay(issuer, owner, mpt(100'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
+            env(tx);
+            env.close();
+            Account const vaultAcct("vault", env.le(keylet)->at(sfAccount));
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
+            env.close();
+
+            Account const charlie{"charlie"};
+            env.fund(XRP(10'000), charlie);
+            env.close();
+            mptt.authorize({.account = charlie});
+            mptt.authorize({.account = issuer, .holder = charlie});
+            env.close();
+
+            auto runTests = [&]() {
+                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+                // Global lock
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+
+                // Global lock → withdraw to issuer
+                // Post-fix: bypasses freeze checks, but accountHolds
+                //           on the pseudo returns 0 under global lock
+                // Pre-fix: checkFrozen(dst=issuer) catches global lock
+                {
+                    auto withdrawToIssuer =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToIssuer[sfDestination] = issuer.human();
+                    env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
+                }
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+                if (fix330Enabled)
+                {
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                }
+                env.close();
+
+                // Vault pseudo-account individual lock
+                mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
+                env.close();
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
+                env.close();
+
+                // Depositor individual lock → self-withdraw blocked
+                // (isDeepFrozen == isFrozen for MPT)
+                mptt.set({.holder = owner, .flags = tfMPTLock});
+                env.close();
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                // Depositor lock → withdraw to 3rd party also blocked
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    env(withdrawToCharlie, Ter(tecLOCKED));
+                }
+
+                // Depositor lock → withdraw to issuer
+                // Post-fix: issuer bypass in checkWithdrawFreezes
+                // Pre-fix: checkFrozen(depositor, share) blocks transitively
+                {
+                    auto withdrawToIssuer =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToIssuer[sfDestination] = issuer.human();
+                    env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
+                }
+                mptt.set({.holder = owner, .flags = tfMPTUnlock});
+                env.close();
+                if (fix330Enabled)
+                {
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                }
+                env.close();
+
+                // 3rd party destination lock → withdraw to 3rd party blocked
+                mptt.set({.holder = charlie, .flags = tfMPTLock});
+                env.close();
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    env(withdrawToCharlie, Ter{tecLOCKED});
+                }
+                // 3rd party lock → self-withdraw unaffected
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                mptt.set({.holder = charlie, .flags = tfMPTUnlock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                env.close();
+
+                // Clawback works while locked
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                env.close();
+            };
+
+            runTests();
+            env.disableFeature(fixCleanup3_3_0);
+            runTests();
+            env.enableFeature(fixCleanup3_3_0);
+        }
+    }
+
 public:
     void
     run() override
@@ -7864,6 +8083,10 @@ public:
         testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice();
         testWithdrawSoleShareholderLoanRepaymentExit();
 
+        testVaultDepositFreeze();
+        testVaultWithdrawFreeze();
+        testVaultSelfWithdrawWhileFrozen();
+
         testReferenceHolding();
         testHoldingDeletionBlocked();
     }
diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h
index 3d813d993c..aca6074c4a 100644
--- a/src/test/jtx/Env.h
+++ b/src/test/jtx/Env.h
@@ -96,6 +96,26 @@ testableAmendments()
     return kIds;
 }
 
+/**
+ * Returns all 2^N permutations of a seed FeatureBitset with each subset of
+ * the given features excluded.  The seed is included as the first element.
+ *
+ * Useful for running a test over every combination of optional amendments
+ * so that each case is exercised both with and without each feature.
+ */
+inline std::vector
+amendmentCombinations(std::initializer_list features, FeatureBitset seed)
+{
+    std::vector result{seed};
+    for (auto const& f : features)
+    {
+        auto const n = result.size();
+        for (std::size_t i = 0; i < n; ++i)
+            result.push_back(result[i] - f);
+    }
+    return result;
+}
+
 //------------------------------------------------------------------------------
 
 class SuiteLogs : public Logs
diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp
index 9256242417..7da419c6e7 100644
--- a/src/test/jtx/impl/utility.cpp
+++ b/src/test/jtx/impl/utility.cpp
@@ -100,5 +100,4 @@ cmdToJSONRPC(std::vector const& args, beast::Journal j, unsigned in
         jv[jss::id] = paramsObj[jss::id];
     return jv;
 }
-
 }  // namespace xrpl::test::jtx
diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h
index 535e14cf1d..c3ed91f672 100644
--- a/src/test/jtx/utility.h
+++ b/src/test/jtx/utility.h
@@ -7,6 +7,7 @@
 #include 
 
 #include 
+#include 
 
 namespace xrpl::test::jtx {
 
@@ -50,5 +51,4 @@ fillSeq(json::Value& jv, ReadView const& view);
 /** Given an xrpld unit test rpc command, return the corresponding JSON. */
 json::Value
 cmdToJSONRPC(std::vector const& args, beast::Journal j, unsigned int apiVersion);
-
 }  // namespace xrpl::test::jtx

From fd8a9152437c3fa42922428e7029828cf54b1b88 Mon Sep 17 00:00:00 2001
From: yinyiqian1 
Date: Fri, 26 Jun 2026 18:26:53 -0400
Subject: [PATCH 020/100] fix: Use trustline balance direction to validate IOU
 PaymentMint/PaymentBurn (#7584)

---
 include/xrpl/protocol/Permissions.h           |   2 +-
 src/libxrpl/protocol/Permissions.cpp          |  22 +-
 .../tx/transactors/payment/Payment.cpp        |  59 +++-
 src/test/app/Delegate_test.cpp                | 318 ++++++++++++++++--
 4 files changed, 365 insertions(+), 36 deletions(-)

diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h
index eb161ef7ad..c6f464082d 100644
--- a/include/xrpl/protocol/Permissions.h
+++ b/include/xrpl/protocol/Permissions.h
@@ -106,7 +106,7 @@ public:
     txToPermissionType(TxType type);
 
     // tx type value is permission value minus one
-    [[nodiscard]] static TxType
+    [[nodiscard]] static std::optional
     permissionToTxType(std::uint32_t value);
 
     /**
diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp
index 3aa9705b03..80aa1b1f4e 100644
--- a/src/libxrpl/protocol/Permissions.cpp
+++ b/src/libxrpl/protocol/Permissions.cpp
@@ -13,6 +13,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -152,9 +153,11 @@ Permission::getPermissionName(std::uint32_t value) const
         return granular;
 
     // not a granular permission, check if it maps to a transaction type
-    auto const txType = permissionToTxType(value);
-    if (auto const* item = TxFormats::getInstance().findByType(txType); item != nullptr)
-        return item->getName();
+    if (auto const txType = permissionToTxType(value))
+    {
+        if (auto const* item = TxFormats::getInstance().findByType(*txType); item != nullptr)
+            return item->getName();
+    }
 
     return std::nullopt;
 }
@@ -231,7 +234,10 @@ Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const
     }
 
     auto const txType = permissionToTxType(permissionValue);
-    auto const txIt = txDelegationMap_.find(txType);
+    if (!txType)
+        return false;
+
+    auto const txIt = txDelegationMap_.find(*txType);
 
     // Tx-level permissions require the transaction type itself to be delegable, and
     // the corresponding amendment enabled.
@@ -245,10 +251,14 @@ Permission::txToPermissionType(TxType const type)
     return static_cast(type) + 1;
 }
 
-TxType
+std::optional
 Permission::permissionToTxType(uint32_t value)
 {
-    XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0");
+    // Values outside this range [1, 65536] would silently truncate when cast to
+    // uint16_t, for example, 65537 would become 1, mapping to the Payment transaction.
+    if (value == 0 || value > std::numeric_limits::max() + 1u)
+        return std::nullopt;
+
     return static_cast(value - 1);
 }
 
diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp
index 9a9a01ec19..7f1e4d8079 100644
--- a/src/libxrpl/tx/transactors/payment/Payment.cpp
+++ b/src/libxrpl/tx/transactors/payment/Payment.cpp
@@ -283,16 +283,59 @@ Payment::checkGranularSemantics(
     if (tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset)
         return terNO_DELEGATE_PERMISSION;
 
-    // PaymentMint and PaymentBurn apply to both IOU and MPT direct payments.
-    if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) &&
-        amountAsset.getIssuer() == tx[sfAccount])
-        return tesSUCCESS;
+    if (isXRP(amountAsset))
+        return terNO_DELEGATE_PERMISSION;
 
-    if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) &&
-        amountAsset.getIssuer() == tx[sfDestination])
-        return tesSUCCESS;
+    return amountAsset.visit(
+        [&](MPTIssue const& mptIssue) -> NotTEC {
+            // For MPT payments, the MPTokenIssuanceID encodes the issuer unambiguously,
+            // unlike IOU, there is no endpoint aliasing where either side of the
+            // trustline can appear as the issuer.
+            if (heldGranularPermissions.contains(PaymentMint) &&
+                mptIssue.getIssuer() == tx[sfAccount])
+                return tesSUCCESS;
+            if (heldGranularPermissions.contains(PaymentBurn) &&
+                mptIssue.getIssuer() == tx[sfDestination])
+                return tesSUCCESS;
+            return terNO_DELEGATE_PERMISSION;
+        },
+        [&](Issue const& issue) -> NotTEC {
+            // For IOU payments, either endpoint may be encoded as the issuer in
+            // sfAmount. PaySteps normalizes those endpoint aliases, so sfAmount.issuer
+            // alone does not reliably identify whether the transaction issues or redeems
+            // IOUs. We determine PaymentMint vs PaymentBurn from the trustline balance
+            // direction instead.
+            auto const account = tx[sfAccount];
+            auto const destination = tx[sfDestination];
 
-    return terNO_DELEGATE_PERMISSION;
+            // Reject if neither endpoint is the issuer.
+            if (issue.getIssuer() != account && issue.getIssuer() != destination)
+                return terNO_DELEGATE_PERMISSION;
+
+            auto const sle = view.read(keylet::trustLine(account, destination, issue.currency));
+            if (!sle)
+                return terNO_DELEGATE_PERMISSION;
+
+            bool const accountIsLow = (account < destination);
+            auto const destLimit = sle->getFieldAmount(accountIsLow ? sfHighLimit : sfLowLimit);
+            auto const rawBalance = sle->getFieldAmount(sfBalance);
+            bool const accountIsHolder =
+                accountIsLow ? rawBalance > beast::kZero : rawBalance < beast::kZero;
+
+            // PaymentMint requires the destination to be the holder and the account to be the
+            // issuer. destLimit > 0: destination is willing to hold account's IOUs (account is the
+            // issuer). !accountIsHolder: DirectStepI will issue, not redeem.
+            if (heldGranularPermissions.contains(PaymentMint) && destLimit > beast::kZero &&
+                !accountIsHolder)
+                return tesSUCCESS;
+
+            // PaymentBurn requires the source account to be the holder and the destination to be
+            // the issuer. accountIsHolder: DirectStepI will redeem, not issue.
+            if (heldGranularPermissions.contains(PaymentBurn) && accountIsHolder)
+                return tesSUCCESS;
+
+            return terNO_DELEGATE_PERMISSION;
+        });
 }
 
 TER
diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp
index 6e80577797..f68f813853 100644
--- a/src/test/app/Delegate_test.cpp
+++ b/src/test/app/Delegate_test.cpp
@@ -53,6 +53,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -1105,10 +1106,43 @@ class Delegate_test : public beast::unit_test::Suite
             env(delegate::set(alice, bob, {"PaymentBurn"}));
             env.close();
 
-            env(pay(alice, gw, usd(30)), Sendmax(usd(30)), delegate::As(bob));
+            env(pay(alice, gw, usd(30)), delegate::As(bob));
             env.require(Balance(alice, usd(20)));
         }
 
+        // PaymentBurn is authorized by balance direction, not trust limit.
+        // holder is allowed to burn even if trust limit is 0.
+        {
+            Env env(*this);
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const gw{"gateway"};
+            auto const gwUSD = gw["USD"];
+            auto const aliceUSD = alice["USD"];
+
+            env.fund(XRP(10000), alice, bob, gw);
+            env.trust(gwUSD(200), alice);
+            env.close();
+
+            env(pay(gw, alice, gwUSD(50)));
+            env.require(Balance(alice, gwUSD(50)));
+            env.close();
+
+            env(delegate::set(alice, bob, {"PaymentBurn"}));
+            env.close();
+
+            env.trust(gwUSD(0), alice);
+            env.close();
+            BEAST_EXPECT(env.limit(alice, gwUSD.issue()) == gwUSD(0));
+
+            env(trust(gw, aliceUSD(200)));
+            env.close();
+
+            env(pay(alice, gw, gwUSD(30)), delegate::As(bob));
+            env.require(Balance(alice, gwUSD(20)));
+            env.require(Balance(gw, aliceUSD(-20)));
+        }
+
         // Test invalid fields or flags not allowed in granular permission template
         {
             Env env(*this, features);
@@ -1184,22 +1218,22 @@ class Delegate_test : public beast::unit_test::Suite
             mpt.authorize({.account = alice});
             mpt.authorize({.account = bob});
 
-            auto const MPT = mpt["MPT"];  // NOLINT(readability-identifier-naming)
-            env(pay(gw, alice, MPT(500)));
-            env(pay(gw, bob, MPT(500)));
+            auto const gwMPT = mpt["MPT"];
+            env(pay(gw, alice, gwMPT(500)));
+            env(pay(gw, bob, gwMPT(500)));
             env.close();
-            auto aliceMPT = env.balance(alice, MPT);
-            auto bobMPT = env.balance(bob, MPT);
+            auto aliceMPT = env.balance(alice, gwMPT);
+            auto bobMPT = env.balance(bob, gwMPT);
 
             // PaymentMint
             {
                 env(delegate::set(gw, bob, {"PaymentMint"}));
                 env.close();
 
-                env(pay(gw, alice, MPT(50)), delegate::As(bob));
-                BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT + MPT(50));
-                BEAST_EXPECT(env.balance(bob, MPT) == bobMPT);
-                aliceMPT = env.balance(alice, MPT);
+                env(pay(gw, alice, gwMPT(50)), delegate::As(bob));
+                BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT + gwMPT(50));
+                BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT);
+                aliceMPT = env.balance(alice, gwMPT);
             }
 
             // PaymentBurn
@@ -1207,26 +1241,235 @@ class Delegate_test : public beast::unit_test::Suite
                 env(delegate::set(alice, bob, {"PaymentBurn"}));
                 env.close();
 
-                env(pay(alice, gw, MPT(50)), delegate::As(bob));
-                BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(50));
-                BEAST_EXPECT(env.balance(bob, MPT) == bobMPT);
-                aliceMPT = env.balance(alice, MPT);
+                env(pay(alice, gw, gwMPT(50)), delegate::As(bob));
+                BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(50));
+                BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT);
+                aliceMPT = env.balance(alice, gwMPT);
             }
 
             // Grant both granular permissions and tx level permission.
             {
                 env(delegate::set(alice, bob, {"PaymentBurn", "PaymentMint", "Payment"}));
                 env.close();
-                env(pay(alice, gw, MPT(50)), delegate::As(bob));
-                BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(50));
-                BEAST_EXPECT(env.balance(bob, MPT) == bobMPT);
-                aliceMPT = env.balance(alice, MPT);
-                env(pay(alice, bob, MPT(100)), delegate::As(bob));
-                BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(100));
-                BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100));
+                env(pay(alice, gw, gwMPT(50)), delegate::As(bob));
+                BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(50));
+                BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT);
+                aliceMPT = env.balance(alice, gwMPT);
+                env(pay(alice, bob, gwMPT(100)), delegate::As(bob));
+                BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(100));
+                BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT + gwMPT(100));
             }
         }
 
+        // PaymentMint/PaymentBurn must not trust IOU issuer aliases.
+        // In a direct IOU payment, sfAmount.issuer may be encoded as either
+        // endpoint, and PaySteps normalizes those aliases to the same execution.
+        // These cases ensure a delegate cannot flip the encoded issuer to turn a
+        // mint into an apparent burn, or a burn into an apparent mint.
+        {
+            Env env(*this);
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const gw{"gateway"};
+            auto const gwUSD = gw["USD"];
+            auto const aliceUSD = alice["USD"];
+
+            env.fund(XRP(10000), alice, bob, gw);
+            env.trust(gwUSD(200), alice);
+            env.close();
+
+            // Alice holds 100 USD issued by gw.
+            env(pay(gw, alice, gwUSD(100)));
+            env.close();
+            env.require(Balance(alice, gwUSD(100)));
+
+            // Delegate with only PaymentBurn tries to mint by encoding
+            // Amount.issuer as the destination alias, alice. The actual issuer
+            // is gw, so this requires PaymentMint and must be rejected.
+            {
+                env(delegate::set(gw, bob, {"PaymentBurn"}));
+                env.close();
+
+                // Amount.issuer = alice (destination), rejected because gw is
+                // the actual issuer and PaymentMint is required.
+                env(pay(gw, alice, aliceUSD(50)),
+                    delegate::As(bob),
+                    Ter(terNO_DELEGATE_PERMISSION));
+                env.require(Balance(alice, gwUSD(100)));
+
+                // Fails because bob holds PaymentBurn, not PaymentMint.
+                env(pay(gw, alice, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
+                env.require(Balance(alice, gwUSD(100)));
+            }
+
+            // Delegate with only PaymentMint tries to burn by encoding
+            // Amount.issuer as the source alias, alice. The actual issuer is
+            // gw, so this requires PaymentBurn and must be rejected.
+            {
+                env(delegate::set(alice, bob, {"PaymentMint"}));
+                env.close();
+
+                // Amount.issuer = alice (account), rejected because gw is the
+                // actual issuer and PaymentBurn is required.
+                env(pay(alice, gw, aliceUSD(50)),
+                    delegate::As(bob),
+                    Ter(terNO_DELEGATE_PERMISSION));
+                env.require(Balance(alice, gwUSD(100)));
+
+                // Fails because bob holds PaymentMint, not PaymentBurn.
+                env(pay(alice, gw, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
+                env.require(Balance(alice, gwUSD(100)));
+            }
+        }
+
+        // Neither account nor destination is issuer.
+        // PaymentMint and PaymentBurn do not authorize these payments.
+        {
+            // IOU
+            {
+                Env env(*this);
+                Account const alice{"alice"};
+                Account const bob{"bob"};
+                Account const gw{"gateway"};
+                Account const gw2{"gateway2"};
+                auto const gwUSD = gw["USD"];
+
+                env.fund(XRP(10000), alice, bob, gw, gw2);
+                env.close();
+
+                env.trust(gwUSD(200), alice);
+                env.close();
+
+                env(pay(gw, alice, gwUSD(100)));
+                env.close();
+
+                env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"}));
+                env.close();
+
+                env(pay(alice, gw, gw2["USD"](50)),
+                    delegate::As(bob),
+                    Ter(terNO_DELEGATE_PERMISSION));
+            }
+
+            // MPT
+            {
+                Env env(*this, features);
+                Account const alice{"alice"};
+                Account const bob{"bob"};
+                Account const gw{"gateway"};
+                Account const gw2{"gateway2"};
+
+                env.fund(XRP(10000), gw2);
+                env.close();
+
+                MPTTester mpt(env, gw, {.holders = {alice, bob}});
+                mpt.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
+
+                mpt.authorize({.account = alice});
+                mpt.authorize({.account = bob});
+
+                auto const gwMPT = mpt["MPT"];
+                env(pay(gw, alice, gwMPT(500)));
+                env(pay(gw, bob, gwMPT(500)));
+                env.close();
+
+                env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"}));
+                env.close();
+
+                env(pay(alice, gw2, gwMPT(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
+            }
+        }
+
+        // IOU issuer is an endpoint, but no trustline exists.
+        {
+            Env env(*this);
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const gw{"gateway"};
+
+            env.fund(XRP(10000), alice, bob, gw);
+            env.close();
+
+            env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"}));
+            env.close();
+
+            env(pay(alice, gw, alice["USD"](50)),
+                delegate::As(bob),
+                Ter(terNO_DELEGATE_PERMISSION));
+        }
+
+        // Both trust limits (who is the designated issuer) and balance direction
+        // (which way DirectStepI executes) must be checked. Neither alone is sufficient.
+        {
+            Env env(*this);
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const gw{"gateway"};
+            auto const gwUSD = gw["USD"];
+            auto const aliceUSD = alice["USD"];
+
+            env.fund(XRP(10000), alice, bob, gw);
+
+            // Alice trusts gw but holds zero gw-issued USD. With balance == 0,
+            // DirectStepI would issue rather than redeem, so PaymentBurn must
+            // be rejected even though Alice's trust limit to gw is positive.
+            {
+                env.trust(gwUSD(200), alice);
+                env.close();
+
+                // Alice has nothing to burn.
+                env(delegate::set(alice, bob, {"PaymentBurn"}));
+                env.close();
+
+                env(pay(alice, gw, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
+                env(pay(alice, gw, aliceUSD(50)),
+                    delegate::As(bob),
+                    Ter(terNO_DELEGATE_PERMISSION));
+            }
+
+            // Set up a trust line where gw holds alice-issued USD. DirectStepI
+            // would redeem rather than issue, so PaymentMint must be rejected
+            // even though the endpoint identity matches.
+            {
+                // Gw sets trust to accept alice-issued USD.
+                env(trust(gw, aliceUSD(200)));
+                env.close();
+
+                // Alice issues her own USD to gw; now gw holds alice's IOUs.
+                env(pay(alice, gw, aliceUSD(100)));
+                env.close();
+
+                // In gw's view, accountHolds(gw, USD, alice) > 0, so DirectStepI redeems.
+                // PaymentMint must be rejected because the step would redeem, not issue.
+                env(delegate::set(gw, bob, {"PaymentMint"}));
+                env.close();
+
+                env(pay(gw, alice, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
+                env(pay(gw, alice, aliceUSD(50)),
+                    delegate::As(bob),
+                    Ter(terNO_DELEGATE_PERMISSION));
+            }
+        }
+
+        // Alice trusts gw but gw is not willing to hold alice's IOU (destLimit == 0).
+        {
+            Env env(*this);
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const gw{"gateway"};
+            env.fund(XRP(10000), alice, bob, gw);
+            env.trust(gw["USD"](200), alice);
+            env.close();
+
+            env(delegate::set(alice, bob, {"PaymentMint"}));
+            env.close();
+
+            env(pay(alice, gw, gw["USD"](50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
+            env(pay(alice, gw, alice["USD"](50)),
+                delegate::As(bob),
+                Ter(terNO_DELEGATE_PERMISSION));
+        }
+
         // Verify granular permissions of different tx types in the same SLE are scoped
         // correctly. AccountSet permissions don't apply to Payment and vice versa
         {
@@ -2617,6 +2860,38 @@ class Delegate_test : public beast::unit_test::Suite
         BEAST_EXPECT(granularPermissions.empty());
     }
 
+    void
+    testPermissionToTxType()
+    {
+        testcase("test Permission to Tx type");
+
+        // 0 is not a valid permission value
+        BEAST_EXPECT(!Permission::permissionToTxType(0));
+
+        // 1 maps to Payment transaction
+        BEAST_EXPECT(Permission::permissionToTxType(1) == ttPAYMENT);
+
+        // UINT16_MAX+1 is the maximum possible tx-level permission value
+        constexpr uint32_t maxTxPermission = std::numeric_limits::max() + 1u;
+        BEAST_EXPECT(Permission::permissionToTxType(maxTxPermission).has_value());
+
+        // exceeding maximum value should return nullopt
+        BEAST_EXPECT(!Permission::permissionToTxType(maxTxPermission + 1));
+
+        // All granular permission values should return nullopt since they do not map to a TxType.
+        for (auto const gp : {
+#pragma push_macro("GRANULAR_PERMISSION")
+#undef GRANULAR_PERMISSION
+#define GRANULAR_PERMISSION(type, txType, value, ...) GranularPermissionType::type,
+#include 
+#undef GRANULAR_PERMISSION
+#pragma pop_macro("GRANULAR_PERMISSION")
+             })
+        {
+            BEAST_EXPECT(!Permission::permissionToTxType(static_cast(gp)));
+        }
+    }
+
     void
     run() override
     {
@@ -2647,6 +2922,7 @@ class Delegate_test : public beast::unit_test::Suite
         testTxDelegableCount();
         testNonDelegableTxWithDelegate(all);
         testDelegateUtilsNullptrCheck();
+        testPermissionToTxType();
     }
 };
 BEAST_DEFINE_TESTSUITE(Delegate, app, xrpl);

From 768d7603b1633d5d9a83801f04de789640882385 Mon Sep 17 00:00:00 2001
From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com>
Date: Fri, 26 Jun 2026 21:20:38 -0400
Subject: [PATCH 021/100] feat: Confidential Transfer for MPT (#5860)

Signed-off-by: dependabot[bot] 
Signed-off-by: chuanshanjida 
Co-authored-by: Ed Hennis 
Co-authored-by: Jingchen 
Co-authored-by: Denis Angell 
Co-authored-by: Bart 
Co-authored-by: yinyiqian1 
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Co-authored-by: Bronek Kozicki 
Co-authored-by: Mayukha Vadari 
Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com>
Co-authored-by: tequ 
Co-authored-by: Ayaz Salikhov 
Co-authored-by: Peter Chen <34582813+PeterChen13579@users.noreply.github.com>
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com>
Co-authored-by: Alex Kremer 
Co-authored-by: Sergey Kuznetsov 
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Tsipenyuk 
Co-authored-by: chuanshanjida 
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Peter Chen 
Co-authored-by: Timothy Banks 
Co-authored-by: Timothy Banks 
---
 CMakeLists.txt                                |    2 +
 conan.lock                                    |    1 +
 conanfile.py                                  |    2 +
 cspell.config.yaml                            |   11 +
 .../xrpl/ledger/helpers/CredentialHelpers.h   |   33 +
 include/xrpl/protocol/ConfidentialTransfer.h  |  431 +
 include/xrpl/protocol/LedgerFormats.h         |    6 +-
 include/xrpl/protocol/Protocol.h              |   62 +
 include/xrpl/protocol/TER.h                   |    6 +
 include/xrpl/protocol/TxFlags.h               |   11 +-
 include/xrpl/protocol/detail/features.macro   |    2 +-
 .../xrpl/protocol/detail/ledger_entries.macro |   23 +-
 include/xrpl/protocol/detail/secp256k1.h      |    5 +-
 include/xrpl/protocol/detail/sfields.macro    |   18 +
 .../xrpl/protocol/detail/transactions.macro   |   87 +
 include/xrpl/protocol/jss.h                   |  330 +-
 .../protocol_autogen/ledger_entries/MPToken.h |  210 +
 .../ledger_entries/MPTokenIssuance.h          |  105 +
 .../transactions/ConfidentialMPTClawback.h    |  201 +
 .../transactions/ConfidentialMPTConvert.h     |  336 +
 .../transactions/ConfidentialMPTConvertBack.h |  310 +
 .../transactions/ConfidentialMPTMergeInbox.h  |  129 +
 .../transactions/ConfidentialMPTSend.h        |  408 +
 .../transactions/MPTokenIssuanceSet.h         |   74 +
 include/xrpl/tx/Transactor.h                  |    5 +
 include/xrpl/tx/invariants/InvariantCheck.h   |    1 +
 include/xrpl/tx/invariants/MPTInvariant.h     |  171 +-
 .../token/ConfidentialMPTClawback.h           |   59 +
 .../token/ConfidentialMPTConvert.h            |   61 +
 .../token/ConfidentialMPTConvertBack.h        |   62 +
 .../token/ConfidentialMPTMergeInbox.h         |   63 +
 .../transactors/token/ConfidentialMPTSend.h   |   72 +
 .../ledger/helpers/CredentialHelpers.cpp      |   54 +-
 src/libxrpl/ledger/helpers/MPTokenHelpers.cpp |    9 +
 src/libxrpl/ledger/helpers/TokenHelpers.cpp   |    3 +-
 src/libxrpl/protocol/ConfidentialTransfer.cpp |  487 +
 src/libxrpl/protocol/PublicKey.cpp            |    3 +-
 src/libxrpl/protocol/TER.cpp                  |    2 +
 src/libxrpl/tx/Transactor.cpp                 |    9 +
 src/libxrpl/tx/invariants/MPTInvariant.cpp    |  302 +
 .../token/ConfidentialMPTClawback.cpp         |  210 +
 .../token/ConfidentialMPTConvert.cpp          |  350 +
 .../token/ConfidentialMPTConvertBack.cpp      |  319 +
 .../token/ConfidentialMPTMergeInbox.cpp       |  150 +
 .../transactors/token/ConfidentialMPTSend.cpp |  445 +
 .../tx/transactors/token/MPTokenAuthorize.cpp |   19 +
 .../token/MPTokenIssuanceCreate.cpp           |   14 +-
 .../transactors/token/MPTokenIssuanceSet.cpp  |  118 +-
 .../app/ConfidentialTransferExtended_test.cpp | 2595 ++++++
 src/test/app/ConfidentialTransfer_test.cpp    | 8208 +++++++++++++++++
 src/test/app/Delegate_test.cpp                |    4 +-
 src/test/app/Invariants_test.cpp              |  249 +
 src/test/app/MPToken_test.cpp                 |    3 +-
 src/test/app/Vault_test.cpp                   |   42 +
 src/test/jtx/ConfidentialTransfer.h           |  496 +
 src/test/jtx/batch.h                          |   40 +-
 src/test/jtx/impl/batch.cpp                   |   11 +
 src/test/jtx/impl/mpt.cpp                     | 1931 +++-
 src/test/jtx/impl/utility.cpp                 |   18 +-
 src/test/jtx/mpt.h                            |  442 +-
 .../ledger_entries/MPTokenIssuanceTests.cpp   |   81 +
 .../ledger_entries/MPTokenTests.cpp           |  162 +
 .../ConfidentialMPTClawbackTests.cpp          |  194 +
 .../ConfidentialMPTConvertBackTests.cpp       |  303 +
 .../ConfidentialMPTConvertTests.cpp           |  309 +
 .../ConfidentialMPTMergeInboxTests.cpp        |  146 +
 .../transactions/ConfidentialMPTSendTests.cpp |  363 +
 .../transactions/MPTokenIssuanceSetTests.cpp  |   42 +
 68 files changed, 21177 insertions(+), 253 deletions(-)
 create mode 100644 include/xrpl/protocol/ConfidentialTransfer.h
 create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h
 create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h
 create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h
 create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h
 create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h
 create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h
 create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h
 create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h
 create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h
 create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTSend.h
 create mode 100644 src/libxrpl/protocol/ConfidentialTransfer.cpp
 create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp
 create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp
 create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp
 create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp
 create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
 create mode 100644 src/test/app/ConfidentialTransferExtended_test.cpp
 create mode 100644 src/test/app/ConfidentialTransfer_test.cpp
 create mode 100644 src/test/jtx/ConfidentialTransfer.h
 create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp
 create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp
 create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp
 create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp
 create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index bdc62442b3..1e8befcc8f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -90,6 +90,7 @@ find_package(ed25519 REQUIRED)
 find_package(gRPC REQUIRED)
 find_package(LibArchive REQUIRED)
 find_package(lz4 REQUIRED)
+find_package(mpt-crypto REQUIRED)
 find_package(nudb REQUIRED)
 find_package(OpenSSL REQUIRED)
 find_package(secp256k1 REQUIRED)
@@ -102,6 +103,7 @@ target_link_libraries(
     INTERFACE
         ed25519::ed25519
         lz4::lz4
+        mpt-crypto::mpt-crypto
         OpenSSL::Crypto
         OpenSSL::SSL
         secp256k1::secp256k1
diff --git a/conan.lock b/conan.lock
index 45dd145914..b6ddfa4e58 100644
--- a/conan.lock
+++ b/conan.lock
@@ -12,6 +12,7 @@
         "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933",
         "openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886",
         "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166",
+        "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",
diff --git a/conanfile.py b/conanfile.py
index 2733d4fc9c..aec4f9eab0 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -30,6 +30,7 @@ class Xrpl(ConanFile):
         "ed25519/2015.03",
         "grpc/1.81.1",
         "libarchive/3.8.7",
+        "mpt-crypto/0.4.0-rc2",
         "nudb/2.0.9",
         "openssl/3.6.3",
         "secp256k1/0.7.1",
@@ -208,6 +209,7 @@ class Xrpl(ConanFile):
             "grpc::grpc++",
             "libarchive::libarchive",
             "lz4::lz4",
+            "mpt-crypto::mpt-crypto",
             "nudb::nudb",
             "openssl::crypto",
             "protobuf::libprotobuf",
diff --git a/cspell.config.yaml b/cspell.config.yaml
index 8273df6c98..c120c31855 100644
--- a/cspell.config.yaml
+++ b/cspell.config.yaml
@@ -60,6 +60,7 @@ words:
   - autobridging
   - bimap
   - bindir
+  - blindings
   - bookdir
   - Bougalis
   - Britto
@@ -95,6 +96,7 @@ words:
   - daria
   - dcmake
   - dearmor
+  - decryptor
   - dedented
   - deleteme
   - demultiplexer
@@ -106,6 +108,7 @@ words:
   - distro
   - doxyfile
   - dxrpl
+  - elgamal
   - enabled
   - enablerepo
   - endmacro
@@ -119,6 +122,7 @@ words:
   - fmtdur
   - fsanitize
   - funclets
+  - Gamal
   - gcov
   - gcovr
   - ghead
@@ -216,6 +220,7 @@ words:
   - partitioner
   - paychan
   - paychans
+  - Pedersen
   - permdex
   - perminute
   - permissioned
@@ -239,6 +244,10 @@ words:
   - Raphson
   - rcflags
   - replayer
+  - rerandomize
+  - rerandomization
+  - rerandomized
+  - rerandomizes
   - rerere
   - retriable
   - RIPD
@@ -255,6 +264,7 @@ words:
   - sahyadri
   - Satoshi
   - scons
+  - Schnorr
   - secp
   - sendq
   - seqit
@@ -285,6 +295,7 @@ words:
   - stvar
   - stvector
   - stxchainattestations
+  - summands
   - superpeer
   - superpeers
   - takergets
diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h
index 0cfbbde538..d6b797ce34 100644
--- a/include/xrpl/ledger/helpers/CredentialHelpers.h
+++ b/include/xrpl/ledger/helpers/CredentialHelpers.h
@@ -63,6 +63,39 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
 TER
 verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);
 
+/**
+ * @brief Check whether src is authorized to deposit to dst.
+ *
+ * @param tx Transaction containing optional credential IDs.
+ * @param view Read-only ledger view.
+ * @param src Source account.
+ * @param dst Destination account.
+ * @param sleDst Destination AccountRoot, if it exists.
+ * @param j Journal for diagnostics.
+ * @return tesSUCCESS if the deposit is allowed, otherwise an authorization
+ *         error.
+ */
+TER
+checkDepositPreauth(
+    STTx const& tx,
+    ReadView const& view,
+    AccountID const& src,
+    AccountID const& dst,
+    std::shared_ptr const& sleDst,
+    beast::Journal j);
+
+/**
+ * @brief Remove expired credentials referenced by the transaction.
+ *
+ * @param tx Transaction containing optional sfCredentialIDs.
+ * @param view Mutable ledger view.
+ * @param j Journal for diagnostics.
+ * @return tesSUCCESS if no referenced credentials expired, tecEXPIRED if any
+ *         were removed, or an error from credential deletion.
+ */
+TER
+cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j);
+
 // Check expired credentials and for existing DepositPreauth ledger object
 TER
 verifyDepositPreauth(
diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h
new file mode 100644
index 0000000000..5b1bcbf606
--- /dev/null
+++ b/include/xrpl/protocol/ConfidentialTransfer.h
@@ -0,0 +1,431 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+
+namespace xrpl {
+
+/**
+ * @brief Bundles an ElGamal public key with its associated encrypted amount.
+ *
+ * Used to represent a recipient in confidential transfers, containing both
+ * the recipient's ElGamal public key and the ciphertext encrypting the
+ * transfer amount under that key.
+ */
+struct ConfidentialRecipient
+{
+    /** @brief The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength). */
+    Slice publicKey;
+
+    /**
+     * @brief The encrypted amount ciphertext
+     * (size=xrpl::kEcGamalEncryptedTotalLength).
+     */
+    Slice encryptedAmount;
+};
+
+/**
+ * @brief Holds two secp256k1 public key components representing an ElGamal
+ * ciphertext (C1, C2).
+ */
+struct EcPair
+{
+    /** @brief First ElGamal ciphertext component. */
+    secp256k1_pubkey c1;
+
+    /** @brief Second ElGamal ciphertext component. */
+    secp256k1_pubkey c2;
+};
+
+/**
+ * @brief Increments the confidential balance version counter on an MPToken.
+ *
+ * The version counter is used to prevent replay attacks by binding proofs
+ * to a specific state of the account's confidential balance. Wraps to 0
+ * on overflow (defined behavior for unsigned integers).
+ *
+ * @param mptoken The MPToken ledger entry to update.
+ */
+inline void
+incrementConfidentialVersion(STObject& mptoken)
+{
+    // Retrieve current version and increment, wrapping back to 0 at UINT32_MAX.
+    // The wrap is computed explicitly rather than relying on unsigned overflow
+    // of `+ 1u`, as it trips the unsigned-integer-overflow sanitizer in the UBSan CI build.
+    auto const current = mptoken[~sfConfidentialBalanceVersion].valueOr(0u);
+    mptoken[sfConfidentialBalanceVersion] =
+        current == std::numeric_limits::max() ? 0u : current + 1u;
+}
+
+/**
+ * @brief Generates the context hash for ConfidentialMPTSend transactions.
+ *
+ * Creates a unique 256-bit hash that binds the zero-knowledge proofs to
+ * this specific send transaction, preventing proof reuse across transactions.
+ *
+ * @param account     The sender's account ID.
+ * @param issuanceID  The MPToken Issuance ID.
+ * @param sequence    The transaction sequence number or ticket number.
+ * @param destination The destination account ID.
+ * @param version     The sender's confidential balance version.
+ * @return A 256-bit context hash unique to this transaction.
+ */
+uint256
+getSendContextHash(
+    AccountID const& account,
+    uint192 const& issuanceID,
+    std::uint32_t sequence,
+    AccountID const& destination,
+    std::uint32_t version);
+
+/**
+ * @brief Generates the context hash for ConfidentialMPTClawback transactions.
+ *
+ * Creates a unique 256-bit hash that binds the equality proof to this
+ * specific clawback transaction.
+ *
+ * @param account    The issuer's account ID.
+ * @param issuanceID The MPToken Issuance ID.
+ * @param sequence   The transaction sequence number or ticket number.
+ * @param holder     The holder's account ID being clawed back from.
+ * @return A 256-bit context hash unique to this transaction.
+ */
+uint256
+getClawbackContextHash(
+    AccountID const& account,
+    uint192 const& issuanceID,
+    std::uint32_t sequence,
+    AccountID const& holder);
+
+/**
+ * @brief Generates the context hash for ConfidentialMPTConvert transactions.
+ *
+ * Creates a unique 256-bit hash that binds the Schnorr proof (for key
+ * registration) to this specific convert transaction.
+ *
+ * @param account    The holder's account ID.
+ * @param issuanceID The MPToken Issuance ID.
+ * @param sequence   The transaction sequence number or a ticket number.
+ * @return A 256-bit context hash unique to this transaction.
+ */
+uint256
+getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence);
+
+/**
+ * @brief Generates the context hash for ConfidentialMPTConvertBack transactions.
+ *
+ * Creates a unique 256-bit hash that binds the zero-knowledge proofs to
+ * this specific convert-back transaction.
+ *
+ * @param account    The holder's account ID.
+ * @param issuanceID The MPToken Issuance ID.
+ * @param sequence   The transaction sequence number or a ticket number.
+ * @param version    The holder's confidential balance version.
+ * @return A 256-bit context hash unique to this transaction.
+ */
+uint256
+getConvertBackContextHash(
+    AccountID const& account,
+    uint192 const& issuanceID,
+    std::uint32_t sequence,
+    std::uint32_t version);
+
+/**
+ * @brief Parses an ElGamal ciphertext into two secp256k1 public key components.
+ *
+ * Breaks an encrypted amount (size=xrpl::kEcGamalEncryptedTotalLength, two
+ * compressed EC points of size=xrpl::kEcCiphertextComponentLength) into
+ * a pair containing (C1, C2) for use in cryptographic operations.
+ *
+ * @param buffer The buffer containing the compressed ciphertext
+ *               (size=xrpl::kEcGamalEncryptedTotalLength).
+ * @return The parsed pair (c1, c2) if successful, std::nullopt if the buffer is invalid.
+ */
+std::optional
+makeEcPair(Slice const& buffer);
+
+/**
+ * @brief Serializes an EcPair into compressed form.
+ *
+ * Converts an EcPair (C1, C2) back into a buffer
+ * (size=xrpl::kEcGamalEncryptedTotalLength) containing two compressed EC
+ * points (size=xrpl::kEcCiphertextComponentLength each).
+ *
+ * @param pair The EcPair to serialize.
+ * @return The buffer (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt
+ *         if serialization fails.
+ */
+std::optional
+serializeEcPair(EcPair const& pair);
+
+/**
+ * @brief Verifies that a buffer contains two valid, parsable EC public keys.
+ *
+ * @param buffer The input buffer containing two concatenated components.
+ * @return true if both components can be parsed successfully, false otherwise.
+ */
+bool
+isValidCiphertext(Slice const& buffer);
+
+/**
+ * @brief Verifies that a buffer contains a valid, parsable compressed EC point.
+ *
+ * Can be used to validate both compressed public keys and Pedersen commitments.
+ * Fails early if the prefix byte is not 0x02 or 0x03.
+ *
+ * @param buffer The input buffer containing a compressed EC point
+ *               (size=xrpl::kCompressedEcPointLength).
+ * @return true if the point can be parsed successfully, false otherwise.
+ */
+bool
+isValidCompressedECPoint(Slice const& buffer);
+
+/**
+ * @brief Homomorphically adds two ElGamal ciphertexts.
+ *
+ * Uses the additive homomorphic property of ElGamal encryption to compute
+ * Enc(a + b) from Enc(a) and Enc(b) without decryption.
+ *
+ * @param a The first ciphertext (size=xrpl::kEcGamalEncryptedTotalLength).
+ * @param b The second ciphertext (size=xrpl::kEcGamalEncryptedTotalLength).
+ * @return The resulting ciphertext Enc(a + b), or std::nullopt on failure.
+ */
+std::optional
+homomorphicAdd(Slice const& a, Slice const& b);
+
+/**
+ * @brief Homomorphically subtracts two ElGamal ciphertexts.
+ *
+ * Uses the additive homomorphic property of ElGamal encryption to compute
+ * Enc(a - b) from Enc(a) and Enc(b) without decryption.
+ *
+ * @param a The minuend ciphertext (size=xrpl::kEcGamalEncryptedTotalLength).
+ * @param b The subtrahend ciphertext (size=xrpl::kEcGamalEncryptedTotalLength).
+ * @return The resulting ciphertext Enc(a - b), or std::nullopt on failure.
+ */
+std::optional
+homomorphicSubtract(Slice const& a, Slice const& b);
+
+/**
+ * @brief Re-randomizes an ElGamal ciphertext without changing its plaintext.
+ *
+ * Adds Enc(0; randomness) under the supplied public key to the ciphertext.
+ * This is used when a public, deterministic scalar must perturb ciphertext
+ * randomness while preserving ledger reproducibility.
+ *
+ * @param ciphertext The ciphertext to re-randomize
+ *                   (size=xrpl::kEcGamalEncryptedTotalLength).
+ * @param pubKeySlice The ElGamal public key matching the ciphertext recipient.
+ * @param randomness The scalar used as zero-encryption randomness
+ *                   (size=xrpl::kEcScalarLength).
+ * @return The re-randomized ciphertext, or std::nullopt on failure.
+ */
+std::optional
+rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness);
+
+/**
+ * @brief Encrypts an amount using ElGamal encryption.
+ *
+ * Produces a ciphertext C = (C1, C2) where C1 = r*G and C2 = m*G + r*Pk,
+ * using the provided blinding factor r.
+ *
+ * @param amt            The plaintext amount to encrypt.
+ * @param pubKeySlice    The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength).
+ * @param blindingFactor The randomness used as blinding factor r
+ *                       (size=xrpl::ecBlindingFactorLength).
+ * @return The ciphertext (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt on failure.
+ */
+std::optional
+encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor);
+
+/**
+ * @brief Generates the canonical zero encryption for a specific MPToken.
+ *
+ * Creates a deterministic encryption of zero that is unique to the account
+ * and MPT issuance. Used to initialize confidential balance fields.
+ *
+ * @param pubKeySlice The holder's ElGamal public key (size=xrpl::kEcPubKeyLength).
+ * @param account     The account ID of the token holder.
+ * @param mptId       The MPToken Issuance ID.
+ * @return The canonical zero ciphertext (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt
+ * on failure.
+ */
+std::optional
+encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId);
+
+/**
+ * @brief Verifies a Schnorr proof of knowledge of an ElGamal private key.
+ *
+ * Proves that the submitter knows the secret key corresponding to the
+ * provided public key, without revealing the secret key itself.
+ *
+ * @param pubKeySlice The ElGamal public key (size=xrpl::kEcPubKeyLength).
+ * @param proofSlice  The Schnorr proof (size=xrpl::ecSchnorrProofLength).
+ * @param contextHash The 256-bit context hash binding the proof.
+ * @return tesSUCCESS if valid, or an error code otherwise.
+ */
+TER
+verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash);
+
+/**
+ * @brief Validates the format of encrypted amount fields in a transaction.
+ *
+ * Checks that all ciphertext fields in the transaction object have the
+ * correct length and contain valid EC points. This function is only used
+ * by ConfidentialMPTConvert and ConfidentialMPTConvertBack transactions.
+ *
+ * @param object The transaction object containing encrypted amount fields.
+ * @return tesSUCCESS if all formats are valid, temMALFORMED if required fields
+ *         are missing, or temBAD_CIPHERTEXT if format validation fails.
+ */
+NotTEC
+checkEncryptedAmountFormat(STObject const& object);
+
+/**
+ * @brief Verifies revealed amount encryptions for all recipients.
+ *
+ * Validates that the same amount was correctly encrypted for the holder,
+ * issuer, and optionally the auditor using their respective public keys.
+ *
+ * @param amount         The revealed plaintext amount.
+ * @param blindingFactor The blinding factor used in all encryptions
+ *                       (size=xrpl::ecBlindingFactorLength).
+ * @param holder         The holder's public key and encrypted amount.
+ * @param issuer         The issuer's public key and encrypted amount.
+ * @param auditor        Optional auditor's public key and encrypted amount.
+ * @return tesSUCCESS if all encryptions are valid, or an error code otherwise.
+ */
+TER
+verifyRevealedAmount(
+    uint64_t const amount,
+    Slice const& blindingFactor,
+    ConfidentialRecipient const& holder,
+    ConfidentialRecipient const& issuer,
+    std::optional const& auditor);
+
+/**
+ * @brief Returns the number of recipients in a confidential transfer.
+ *
+ * Returns 4 if an auditor is present (sender, destination, issuer, auditor),
+ * or 3 if no auditor (sender, destination, issuer).
+ *
+ * @param hasAuditor Whether the issuance has an auditor configured.
+ * @return The number of recipients (3 or 4).
+ */
+constexpr uint8_t
+getConfidentialRecipientCount(bool hasAuditor)
+{
+    return hasAuditor ? 4 : 3;
+}
+
+/**
+ * @brief Verifies a compact sigma clawback proof.
+ *
+ * Proves that the issuer knows the exact amount encrypted in the holder's
+ * balance ciphertext. Used in ConfidentialMPTClawback to verify the issuer
+ * can decrypt the balance using their private key.
+ *
+ * @param amount      The revealed plaintext amount.
+ * @param proof       The zero-knowledge proof bytes (ecClawbackProofLength).
+ * @param pubKeySlice The issuer's ElGamal public key (kEcPubKeyLength bytes).
+ * @param ciphertext  The issuer's encrypted balance on the holder's account
+ *                    (kEcGamalEncryptedTotalLength bytes).
+ * @param contextHash The 256-bit context hash binding the proof.
+ * @return tesSUCCESS if the proof is valid, or an error code otherwise.
+ */
+TER
+verifyClawbackProof(
+    uint64_t const amount,
+    Slice const& proof,
+    Slice const& pubKeySlice,
+    Slice const& ciphertext,
+    uint256 const& contextHash);
+
+/**
+ * @brief Generates a cryptographically secure blinding factor
+ * (size=xrpl::kEcBlindingFactorLength).
+ *
+ * Produces random bytes suitable for use as an ElGamal blinding factor
+ * or Pedersen commitment randomness.
+ *
+ * @return A buffer containing the random blinding factor
+ *         (size=xrpl::kEcBlindingFactorLength).
+ */
+Buffer
+generateBlindingFactor();
+
+/**
+ * @brief Verifies all zero-knowledge proofs for a ConfidentialMPTSend transaction.
+ *
+ * This function calls mpt_verify_send_proof API in the mpt-crypto utility lib, which verifies the
+ * equality proof, amount linkage, balance linkage, and range proof.
+ * Equality proof: Proves the same value is encrypted for the sender, receiver, issuer, and auditor.
+ * Amount linkage: Proves the send amount matches the amount Pedersen commitment.
+ * Balance linkage: Proves the sender's balance matches the balance Pedersen
+ * commitment.
+ * Range proof: Proves the amount and the remaining balance are within range [0, 2^64-1].
+ *
+ * @param proof             The full proof blob.
+ * @param sender            The sender's public key and encrypted amount.
+ * @param destination       The destination's public key and encrypted amount.
+ * @param issuer            The issuer's public key and encrypted amount.
+ * @param auditor           The auditor's public key and encrypted amount if present.
+ * @param spendingBalance   The sender's current spending balance ciphertext.
+ * @param amountCommitment  The Pedersen commitment to the send amount.
+ * @param balanceCommitment The Pedersen commitment to the sender's balance.
+ * @param contextHash       The context hash binding the proof.
+ * @return tesSUCCESS if all proofs are valid, or an error code otherwise.
+ */
+TER
+verifySendProof(
+    Slice const& proof,
+    ConfidentialRecipient const& sender,
+    ConfidentialRecipient const& destination,
+    ConfidentialRecipient const& issuer,
+    std::optional const& auditor,
+    Slice const& spendingBalance,
+    Slice const& amountCommitment,
+    Slice const& balanceCommitment,
+    uint256 const& contextHash);
+
+/**
+ * @brief Verifies all zero-knowledge proofs for a ConfidentialMPTConvertBack transaction.
+ *
+ * This function calls mpt_verify_convert_back_proof API in the mpt-crypto utility lib, which
+ * verifies the balance linkage proof and range proof. Balance linkage proof: proves the balance
+ * commitment matches the spending ciphertext. Range proof: proves the remaining balance after
+ * convert back is within range [0, 2^64-1].
+ *
+ * @param proof             The full proof blob.
+ * @param pubKeySlice       The holder's public key.
+ * @param spendingBalance   The holder's spending balance ciphertext.
+ * @param balanceCommitment The Pedersen commitment to the balance.
+ * @param amount            The amount being converted back to public.
+ * @param contextHash       The context hash binding the proof.
+ * @return tesSUCCESS if all proofs are valid, or an error code otherwise.
+ */
+TER
+verifyConvertBackProof(
+    Slice const& proof,
+    Slice const& pubKeySlice,
+    Slice const& spendingBalance,
+    Slice const& balanceCommitment,
+    uint64_t amount,
+    uint256 const& contextHash);
+
+}  // namespace xrpl
diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h
index c1274e9e91..70afd12f34 100644
--- a/include/xrpl/protocol/LedgerFormats.h
+++ b/include/xrpl/protocol/LedgerFormats.h
@@ -177,7 +177,8 @@ enum LedgerEntryType : std::uint16_t {
         LSF_FLAG(lsfMPTCanEscrow, 0x00000008)                                                                                      \
         LSF_FLAG(lsfMPTCanTrade, 0x00000010)                                                                                       \
         LSF_FLAG(lsfMPTCanTransfer, 0x00000020)                                                                                    \
-        LSF_FLAG(lsfMPTCanClawback, 0x00000040))                                                                                   \
+        LSF_FLAG(lsfMPTCanClawback, 0x00000040)                                                                                    \
+        LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080))                                                                         \
                                                                                                                                    \
     LEDGER_OBJECT(MPTokenIssuanceMutable,                                                                                          \
         LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002)                                                                              \
@@ -186,8 +187,9 @@ enum LedgerEntryType : std::uint16_t {
         LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010)                                                                             \
         LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020)                                                                          \
         LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040)                                                                          \
+        LSF_FLAG(lsmfMPTCannotEnableCanHoldConfidentialBalance, 0x00000080)                                                                          \
         LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000)                                                                             \
-        LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000))                                                                         \
+        LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000))                                                            \
                                                                                                                                    \
     LEDGER_OBJECT(MPToken,                                                                                                         \
         LSF_FLAG2(lsfMPTLocked, 0x00000001)                                                                                        \
diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h
index 6a96b2ccbe..7eac92e83c 100644
--- a/include/xrpl/protocol/Protocol.h
+++ b/include/xrpl/protocol/Protocol.h
@@ -4,6 +4,10 @@
 #include 
 #include 
 
+#include 
+#include 
+
+#include 
 #include 
 
 namespace xrpl {
@@ -307,4 +311,62 @@ constexpr std::size_t kPermissionMaxSize = 10;
 /** The maximum number of transactions that can be in a batch. */
 constexpr std::size_t kMaxBatchTxCount = 8;
 
+/** Length of a secp256k1 scalar in bytes. */
+constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE;
+
+/** Length of EC point (compressed) */
+constexpr std::size_t kCompressedEcPointLength = 33;
+
+/** Length of one compressed EC point component in an EC ElGamal ciphertext. */
+constexpr std::size_t kEcCiphertextComponentLength = kMPT_ELGAMAL_CIPHER_SIZE;
+
+/** EC ElGamal ciphertext length: two compressed EC points concatenated. */
+constexpr std::size_t kEcGamalEncryptedTotalLength = kMPT_ELGAMAL_TOTAL_SIZE;
+
+/** Length of EC public key (compressed) */
+constexpr std::size_t kEcPubKeyLength = kMPT_PUBKEY_SIZE;
+
+/** Length of EC private key in bytes */
+constexpr std::size_t kEcPrivKeyLength = kMPT_PRIVKEY_SIZE;
+
+/** Length of the EC blinding factor in bytes */
+constexpr std::size_t kEcBlindingFactorLength = kMPT_BLINDING_FACTOR_SIZE;
+
+/** Length of Schnorr ZKProof for public key registration (compact form) in bytes */
+constexpr std::size_t kEcSchnorrProofLength = kMPT_SCHNORR_PROOF_SIZE;
+
+/** Length of Pedersen Commitment (compressed) */
+constexpr std::size_t kEcPedersenCommitmentLength = kMPT_PEDERSEN_COMMIT_SIZE;
+
+/** Length of single bulletproof (range proof for 1 commitment) in bytes */
+constexpr std::size_t kEcSingleBulletproofLength = kMPT_SINGLE_BULLETPROOF_SIZE;
+
+/** Length of double bulletproof (range proof for 2 commitments) in bytes */
+constexpr std::size_t kEcDoubleBulletproofLength = kMPT_DOUBLE_BULLETPROOF_SIZE;
+
+/** Length of the compact sigma proof component for ConfidentialMPTSend. */
+constexpr std::size_t kEcSendSigmaProofLength = SECP256K1_COMPACT_STANDARD_PROOF_SIZE;
+
+/**  192 bytes compact sigma proof + 754 bytes double bulletproof. */
+constexpr std::size_t kEcSendProofLength = kEcSendSigmaProofLength + kEcDoubleBulletproofLength;
+
+/** Length of the compact sigma proof component for ConfidentialMPTConvertBack. */
+constexpr std::size_t kEcConvertBackSigmaProofLength = SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE;
+
+/**  128 bytes compact sigma proof + 688 bytes single bulletproof. */
+constexpr std::size_t kEcConvertBackProofLength =
+    kEcConvertBackSigmaProofLength + kEcSingleBulletproofLength;
+
+/** Length of the ZKProof for ConfidentialMPTClawback. */
+constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE;
+
+/** Extra base fee multiplier charged to confidential MPT transactions. */
+constexpr std::uint32_t kConfidentialFeeMultiplier = 9;
+
+/** Compressed EC point prefix for even y-coordinate */
+constexpr std::uint8_t kEcCompressedPrefixEvenY = 0x02;
+
+/** Compressed EC point prefix for odd y-coordinate */
+constexpr std::uint8_t kEcCompressedPrefixOddY = 0x03;
+
 }  // namespace xrpl
diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h
index 072bd4778f..84c344ea76 100644
--- a/include/xrpl/protocol/TER.h
+++ b/include/xrpl/protocol/TER.h
@@ -128,6 +128,7 @@ enum TEMcodes : TERUnderlyingType {
     temBAD_TRANSFER_FEE,
     temINVALID_INNER_BATCH,
     temBAD_MPT,
+    temBAD_CIPHERTEXT,
 };
 
 //------------------------------------------------------------------------------
@@ -358,6 +359,11 @@ enum TECcodes : TERUnderlyingType {
     tecLIMIT_EXCEEDED = 195,
     tecPSEUDO_ACCOUNT = 196,
     tecPRECISION_LOSS = 197,
+    // DEPRECATED: This error code tecNO_DELEGATE_PERMISSION is reserved for
+    // backward compatibility with historical data on non-prod networks, can be
+    // reclaimed after those networks reset.
+    tecNO_DELEGATE_PERMISSION = 198,
+    tecBAD_PROOF = 199,
 };
 
 //------------------------------------------------------------------------------
diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h
index f9c7bc1a5d..461afd24e7 100644
--- a/include/xrpl/protocol/TxFlags.h
+++ b/include/xrpl/protocol/TxFlags.h
@@ -140,7 +140,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
         TF_FLAG(tfMPTCanEscrow, lsfMPTCanEscrow)                                                                                                               \
         TF_FLAG(tfMPTCanTrade, lsfMPTCanTrade)                                                                                                                 \
         TF_FLAG(tfMPTCanTransfer, lsfMPTCanTransfer)                                                                                                           \
-        TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback),                                                                                                          \
+        TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback)                                                                                                           \
+        TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance),                                                                                                            \
         MASK_ADJ(0))                                                                                                                                           \
                                                                                                                                                                \
     TRANSACTION(MPTokenAuthorize,                                                                                                                              \
@@ -349,10 +350,13 @@ inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTrans
 inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback;
 inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
 inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
+inline constexpr FlagValue tmfMPTCannotEnableCanHoldConfidentialBalance =
+    lsmfMPTCannotEnableCanHoldConfidentialBalance;
 inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask =
     ~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow |
       tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback |
-      tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
+      tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee |
+      tmfMPTCannotEnableCanHoldConfidentialBalance);
 
 // MPTokenIssuanceSet MutableFlags:
 // Enable mutable capability flags. These flags are one-way: once enabled,
@@ -364,9 +368,10 @@ inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004;
 inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008;
 inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010;
 inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020;
+inline constexpr FlagValue tmfMPTSetCanHoldConfidentialBalance = 0x00000040;
 inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask =
     ~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade |
-      tmfMPTSetCanTransfer | tmfMPTSetCanClawback);
+      tmfMPTSetCanTransfer | tmfMPTSetCanClawback | tmfMPTSetCanHoldConfidentialBalance);
 
 // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a
 // TrustLine to be added to the issuer of that token without explicit permission from that issuer.
diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index 2b6beaa671..a9b237be32 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -14,7 +14,7 @@
 
 // Add new amendments to the top of this list.
 // Keep it sorted in reverse chronological order.
-
+XRPL_FEATURE(ConfidentialTransfer,        Supported::No, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_3_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_2_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(MPTokensV2,                  Supported::No,  VoteBehavior::DefaultNo)
diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro
index e0ea1a61c3..2f403708c3 100644
--- a/include/xrpl/protocol/detail/ledger_entries.macro
+++ b/include/xrpl/protocol/detail/ledger_entries.macro
@@ -401,19 +401,28 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
     {sfDomainID,                 SoeOptional},
     {sfMutableFlags,             SoeDefault},
     {sfReferenceHolding,         SoeOptional},
+    {sfIssuerEncryptionKey,      SoeOptional},
+    {sfAuditorEncryptionKey,     SoeOptional},
+    {sfConfidentialOutstandingAmount, SoeDefault},
 }))
 
 /** A ledger object which tracks MPToken
     \sa keylet::mptoken
  */
 LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
-    {sfAccount,                  SoeRequired},
-    {sfMPTokenIssuanceID,        SoeRequired},
-    {sfMPTAmount,                SoeDefault},
-    {sfLockedAmount,             SoeOptional},
-    {sfOwnerNode,                SoeRequired},
-    {sfPreviousTxnID,            SoeRequired},
-    {sfPreviousTxnLgrSeq,        SoeRequired},
+    {sfAccount,                     SoeRequired},
+    {sfMPTokenIssuanceID,           SoeRequired},
+    {sfMPTAmount,                   SoeDefault},
+    {sfLockedAmount,                SoeOptional},
+    {sfOwnerNode,                   SoeRequired},
+    {sfPreviousTxnID,               SoeRequired},
+    {sfPreviousTxnLgrSeq,           SoeRequired},
+    {sfConfidentialBalanceInbox,    SoeOptional},
+    {sfConfidentialBalanceSpending, SoeOptional},
+    {sfConfidentialBalanceVersion,  SoeDefault},
+    {sfIssuerEncryptedBalance,      SoeOptional},
+    {sfAuditorEncryptedBalance,     SoeOptional},
+    {sfHolderEncryptionKey,         SoeOptional},
 }))
 
 /** A ledger object which tracks Oracle
diff --git a/include/xrpl/protocol/detail/secp256k1.h b/include/xrpl/protocol/detail/secp256k1.h
index 17dfa3ff25..5ca9033eae 100644
--- a/include/xrpl/protocol/detail/secp256k1.h
+++ b/include/xrpl/protocol/detail/secp256k1.h
@@ -11,7 +11,10 @@ secp256k1Context()
     struct Holder
     {
         secp256k1_context* impl;
-        Holder() : impl(secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
+        // SECP256K1_CONTEXT_SIGN and SECP256K1_CONTEXT_VERIFY were deprecated.
+        // All contexts support both signing and verification, so
+        // SECP256K1_CONTEXT_NONE is the correct flag to use.
+        Holder() : impl(secp256k1_context_create(SECP256K1_CONTEXT_NONE))
         {
         }
 
diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro
index 01bb4fc480..0d453eea11 100644
--- a/include/xrpl/protocol/detail/sfields.macro
+++ b/include/xrpl/protocol/detail/sfields.macro
@@ -113,6 +113,7 @@ TYPED_SFIELD(sfInterestRate,             UINT32,    65) // 1/10 basis points (bi
 TYPED_SFIELD(sfLateInterestRate,         UINT32,    66) // 1/10 basis points (bips)
 TYPED_SFIELD(sfCloseInterestRate,        UINT32,    67) // 1/10 basis points (bips)
 TYPED_SFIELD(sfOverpaymentInterestRate,  UINT32,    68) // 1/10 basis points (bips)
+TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32,  69)
 
 // 64-bit integers (common)
 TYPED_SFIELD(sfIndexNext,                UINT64,     1)
@@ -146,6 +147,7 @@ TYPED_SFIELD(sfSubjectNode,              UINT64,    28)
 TYPED_SFIELD(sfLockedAmount,             UINT64,    29, SField::kSmdBaseTen|SField::kSmdDefault)
 TYPED_SFIELD(sfVaultNode,                UINT64,    30)
 TYPED_SFIELD(sfLoanBrokerNode,           UINT64,    31)
+TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::kSmdBaseTen|SField::kSmdDefault)
 
 // 128-bit
 TYPED_SFIELD(sfEmailHash,                UINT128,    1)
@@ -206,6 +208,7 @@ TYPED_SFIELD(sfLoanBrokerID,             UINT256,   37,
     SField::kSmdPseudoAccount | SField::kSmdDefault)
 TYPED_SFIELD(sfLoanID,                   UINT256,   38)
 TYPED_SFIELD(sfReferenceHolding,         UINT256,   39)
+TYPED_SFIELD(sfBlindingFactor,           UINT256,   40)
 
 // number (common)
 TYPED_SFIELD(sfNumber,                   NUMBER,     1)
@@ -299,6 +302,21 @@ TYPED_SFIELD(sfAssetClass,               VL,        28)
 TYPED_SFIELD(sfProvider,                 VL,        29)
 TYPED_SFIELD(sfMPTokenMetadata,          VL,        30)
 TYPED_SFIELD(sfCredentialType,           VL,        31)
+TYPED_SFIELD(sfConfidentialBalanceInbox,    VL,     32)
+TYPED_SFIELD(sfConfidentialBalanceSpending, VL,     33)
+TYPED_SFIELD(sfIssuerEncryptedBalance,      VL,     34)
+TYPED_SFIELD(sfIssuerEncryptionKey,         VL,     35)
+TYPED_SFIELD(sfHolderEncryptionKey,         VL,     36)
+TYPED_SFIELD(sfZKProof,                     VL,     37)
+TYPED_SFIELD(sfHolderEncryptedAmount,       VL,     38)
+TYPED_SFIELD(sfIssuerEncryptedAmount,       VL,     39)
+TYPED_SFIELD(sfSenderEncryptedAmount,       VL,     40)
+TYPED_SFIELD(sfDestinationEncryptedAmount,  VL,     41)
+TYPED_SFIELD(sfAuditorEncryptedBalance,     VL,     42)
+TYPED_SFIELD(sfAuditorEncryptedAmount,      VL,     43)
+TYPED_SFIELD(sfAuditorEncryptionKey,        VL,     44)
+TYPED_SFIELD(sfAmountCommitment,            VL,     45)
+TYPED_SFIELD(sfBalanceCommitment,           VL,     46)
 
 // account (common)
 TYPED_SFIELD(sfAccount,                  ACCOUNT,    1)
diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro
index dbaaf46083..8a3b0ff2ce 100644
--- a/include/xrpl/protocol/detail/transactions.macro
+++ b/include/xrpl/protocol/detail/transactions.macro
@@ -735,6 +735,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
     {sfMPTokenMetadata, SoeOptional},
     {sfTransferFee, SoeOptional},
     {sfMutableFlags, SoeOptional},
+    {sfIssuerEncryptionKey, SoeOptional},
+    {sfAuditorEncryptionKey, SoeOptional},
 }))
 
 /** This transaction type authorizes a MPToken instance */
@@ -1077,6 +1079,91 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
     {sfAmount, SoeRequired, SoeMptSupported},
 }))
 
+/** This transaction type converts into confidential MPT balance. */
+#if TRANSACTION_INCLUDE
+#   include 
+#endif
+TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert,
+    Delegation::Delegable,
+    featureConfidentialTransfer,
+    NoPriv,
+    ({
+    {sfMPTokenIssuanceID, SoeRequired},
+    {sfMPTAmount, SoeRequired},
+    {sfHolderEncryptionKey, SoeOptional},
+    {sfHolderEncryptedAmount, SoeRequired},
+    {sfIssuerEncryptedAmount, SoeRequired},
+    {sfAuditorEncryptedAmount, SoeOptional},
+    {sfBlindingFactor, SoeRequired},
+    {sfZKProof, SoeOptional},
+}))
+
+/** This transaction type merges MPT inbox. */
+#if TRANSACTION_INCLUDE
+#   include 
+#endif
+TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox,
+    Delegation::Delegable,
+    featureConfidentialTransfer,
+    NoPriv,
+    ({
+    {sfMPTokenIssuanceID, SoeRequired},
+}))
+
+/** This transaction type converts back into public MPT balance. */
+#if TRANSACTION_INCLUDE
+#   include 
+#endif
+TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack,
+    Delegation::Delegable,
+    featureConfidentialTransfer,
+    NoPriv,
+    ({
+    {sfMPTokenIssuanceID, SoeRequired},
+    {sfMPTAmount, SoeRequired},
+    {sfHolderEncryptedAmount, SoeRequired},
+    {sfIssuerEncryptedAmount, SoeRequired},
+    {sfAuditorEncryptedAmount, SoeOptional},
+    {sfBlindingFactor, SoeRequired},
+    {sfZKProof, SoeRequired},
+    {sfBalanceCommitment, SoeRequired},
+}))
+
+#if TRANSACTION_INCLUDE
+#   include 
+#endif
+TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend,
+    Delegation::Delegable,
+    featureConfidentialTransfer,
+    NoPriv,
+    ({
+    {sfMPTokenIssuanceID, SoeRequired},
+    {sfDestination, SoeRequired},
+    {sfDestinationTag, SoeOptional},
+    {sfSenderEncryptedAmount, SoeRequired},
+    {sfDestinationEncryptedAmount, SoeRequired},
+    {sfIssuerEncryptedAmount, SoeRequired},
+    {sfAuditorEncryptedAmount, SoeOptional},
+    {sfZKProof, SoeRequired},
+    {sfAmountCommitment, SoeRequired},
+    {sfBalanceCommitment, SoeRequired},
+    {sfCredentialIDs, SoeOptional},
+}))
+
+#if TRANSACTION_INCLUDE
+#   include 
+#endif
+TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
+    Delegation::Delegable,
+    featureConfidentialTransfer,
+    NoPriv,
+    ({
+    {sfMPTokenIssuanceID, SoeRequired},
+    {sfHolder, SoeRequired},
+    {sfMPTAmount, SoeRequired},
+    {sfZKProof, SoeRequired},
+}))
+
 /** This system-generated transaction type is used to update the status of the various amendments.
 
     For details, see: https://xrpl.org/amendments.html
diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h
index 8a2a112542..191ed385f3 100644
--- a/include/xrpl/protocol/jss.h
+++ b/include/xrpl/protocol/jss.h
@@ -137,6 +137,7 @@ JSS(authorized_credentials);      // in: ledger_entry DepositPreauth
 JSS(auth_accounts);               // out: amm_info
 JSS(auth_change);                 // out: AccountInfo
 JSS(auth_change_queued);          // out: AccountInfo
+JSS(auditor_encrypted_balance);   // out: mpt_holders (confidential MPT)
 JSS(available);                   // out: ValidatorList
 JSS(avg_bps_recv);                // out: Peers
 JSS(avg_bps_sent);                // out: Peers
@@ -161,9 +162,6 @@ JSS(build_path);                  // in: TransactionSign
 JSS(build_version);               // out: NetworkOPs
 JSS(cancel_after);                // out: AccountChannels
 JSS(can_delete);                  // out: CanDelete
-JSS(mpt_amount);                  // out: mpt_holders
-JSS(mpt_issuance_id);             // in: Payment, mpt_holders
-JSS(mptoken_index);               // out: mpt_holders
 JSS(changes);                     // out: BookChanges
 JSS(channel_id);                  // out: AccountChannels
 JSS(channels);                    // out: AccountChannels
@@ -185,165 +183,170 @@ JSS(command);                     // in: RPCHandler
 JSS(common);                      // out: RPC server_definitions
 JSS(complete);                    // out: NetworkOPs, InboundLedger
 JSS(complete_ledgers);            // out: NetworkOPs, PeerImp
-JSS(consensus);                   // out: NetworkOPs, LedgerConsensus
-JSS(converge_time);               // out: NetworkOPs
-JSS(converge_time_s);             // out: NetworkOPs
-JSS(cookie);                      // out: NetworkOPs
-JSS(count);                       // in: AccountTx*, ValidatorList
-JSS(counters);                    // in/out: retrieve counters
-JSS(credentials);                 // in: deposit_authorized
-JSS(credential_type);             // in: LedgerEntry DepositPreauth
-JSS(ctid);                        // in/out: Tx RPC
-JSS(currency_a);                  // out: BookChanges
-JSS(currency_b);                  // out: BookChanges
-JSS(currency);                    // in: paths/PathRequest, STAmount
-                                  // out: STPathSet, STAmount, AccountLines
-JSS(current);                     // out: OwnerInfo
-JSS(current_activities);          //
-JSS(current_ledger_size);         // out: TxQ
-JSS(current_queue_size);          // out: TxQ
-JSS(data);                        // out: LedgerData
-JSS(date);                        // out: tx/Transaction, NetworkOPs
-JSS(dbKBLedger);                  // out: getCounts
-JSS(dbKBTotal);                   // out: getCounts
-JSS(dbKBTransaction);             // out: getCounts
-JSS(debug_signing);               // in: TransactionSign
-JSS(deletion_blockers_only);      // in: AccountObjects
-JSS(delivered_amount);            // out: insertDeliveredAmount
-JSS(deposit_authorized);          // out: deposit_authorized
-JSS(deprecated);                  //
-JSS(descending);                  // in: AccountTx*
-JSS(description);                 // in/out: Reservations
-JSS(destination);                 // in: nft_buy_offers, nft_sell_offers
-JSS(destination_account);         // in: PathRequest, RipplePathFind, account_lines
-                                  // out: AccountChannels
-JSS(destination_amount);          // in: PathRequest, RipplePathFind
-JSS(destination_currencies);      // in: PathRequest, RipplePathFind
-JSS(destination_tag);             // in: PathRequest
-                                  // out: AccountChannels
-JSS(details);                     // out: Manifest, server_info
-JSS(dir_entry);                   // out: DirectoryEntryIterator
-JSS(dir_index);                   // out: DirectoryEntryIterator
-JSS(dir_root);                    // out: DirectoryEntryIterator
-JSS(discounted_fee);              // out: amm_info
-JSS(domain);                      // out: ValidatorInfo, Manifest
-JSS(drops);                       // out: TxQ
-JSS(duration_us);                 // out: NetworkOPs
-JSS(effective);                   // out: ValidatorList
-                                  // in: UNL
-JSS(enabled);                     // out: AmendmentTable
-JSS(engine_result);               // out: NetworkOPs, TransactionSign, Submit
-JSS(engine_result_code);          // out: NetworkOPs, TransactionSign, Submit
-JSS(engine_result_message);       // out: NetworkOPs, TransactionSign, Submit
-JSS(entire_set);                  // out: get_aggregate_price
-JSS(ephemeral_key);               // out: ValidatorInfo
-                                  // in/out: Manifest
-JSS(error);                       // out: error
-JSS(errored);                     //
-JSS(error_code);                  // out: error
-JSS(error_exception);             // out: Submit
-JSS(error_message);               // out: error
-JSS(expand);                      // in: handler/Ledger
-JSS(expected_date);               // out: any (warnings)
-JSS(expected_date_UTC);           // out: any (warnings)
-JSS(expected_ledger_size);        // out: TxQ
-JSS(expiration);                  // out: AccountOffers, AccountChannels, ValidatorList, amm_info
-JSS(fail_hard);                   // in: Sign, Submit
-JSS(failed);                      // out: InboundLedger
-JSS(feature);                     // in: Feature
-JSS(features);                    // out: Feature
-JSS(fee_base);                    // out: NetworkOPs
-JSS(fee_div_max);                 // in: TransactionSign
-JSS(fee_level);                   // out: AccountInfo
-JSS(fee_mult_max);                // in: TransactionSign
-JSS(fee_ref);                     // out: NetworkOPs, DEPRECATED
-JSS(fetch_pack);                  // out: NetworkOPs
-JSS(FIELDS);                      // out: RPC server_definitions
-                                  // matches definitions.json format
-JSS(first);                       // out: rpc/Version
-JSS(finished);                    //
-JSS(fix_txns);                    // in: LedgerCleaner
-JSS(flags);                       // out: AccountOffers, NetworkOPs
-JSS(forward);                     // in: AccountTx
-JSS(freeze);                      // out: AccountLines
-JSS(freeze_peer);                 // out: AccountLines
-JSS(deep_freeze);                 // out: AccountLines
-JSS(deep_freeze_peer);            // out: AccountLines
-JSS(frozen_balances);             // out: GatewayBalances
-JSS(full);                        // in: LedgerClearer, handlers/Ledger
-JSS(full_reply);                  // out: PathFind
-JSS(fullbelow_size);              // out: GetCounts
-JSS(git);                         // out: server_info
-JSS(good);                        // out: RPCVersion
-JSS(hash);                        // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field
-JSS(have_header);                 // out: InboundLedger
-JSS(have_state);                  // out: InboundLedger
-JSS(have_transactions);           // out: InboundLedger
-JSS(high);                        // out: BookChanges
-JSS(highest_sequence);            // out: AccountInfo
-JSS(highest_ticket);              // out: AccountInfo
-JSS(historical_perminute);        // historical_perminute.
-JSS(holders);                     // out: MPTHolders
-JSS(hostid);                      // out: NetworkOPs
-JSS(hotwallet);                   // in: GatewayBalances
-JSS(id);                          // websocket.
-JSS(ident);                       // in: AccountCurrencies, AccountInfo, OwnerInfo
-JSS(ignore_default);              // in: AccountLines
-JSS(in);                          // out: OverlayImpl
-JSS(inLedger);                    // out: tx/Transaction
-JSS(inbound);                     // out: PeerImp
-JSS(index);                       // in: LedgerEntry
-                                  // out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData
-JSS(info);                        // out: ServerInfo, ConsensusInfo, FetchInfo
-JSS(initial_sync_duration_us);    //
-JSS(internal_command);            // in: Internal
-JSS(invalid_API_version);         // out: Many, when a request has an invalid version
-JSS(io_latency_ms);               // out: NetworkOPs
-JSS(ip);                          // in: Connect, out: OverlayImpl
-JSS(is_burned);                   // out: nft_info (clio)
-JSS(isSerialized);                // out: RPC server_definitions
-                                  // matches definitions.json format
-JSS(isSigningField);              // out: RPC server_definitions
-                                  // matches definitions.json format
-JSS(isVLEncoded);                 // out: RPC server_definitions
-                                  // matches definitions.json format
-JSS(issuer);                      // in: RipplePathFind, Subscribe, Unsubscribe, BookOffers
-                                  // out: STPathSet, STAmount
-JSS(job);                         //
-JSS(job_queue);                   //
-JSS(jobs);                        //
-JSS(jsonrpc);                     // json version
-JSS(jq_trans_overflow);           // JobQueue transaction limit overflow.
-JSS(kept);                        // out: SubmitTransaction
-JSS(key);                         // out
-JSS(key_type);                    // in/out: WalletPropose, TransactionSign
-JSS(latency);                     // out: PeerImp
-JSS(last);                        // out: RPCVersion
-JSS(last_close);                  // out: NetworkOPs
-JSS(last_refresh_time);           // out: ValidatorSite
-JSS(last_refresh_status);         // out: ValidatorSite
-JSS(last_refresh_message);        // out: ValidatorSite
-JSS(ledger);                      // in: NetworkOPs, LedgerCleaner, RPCHelpers
-                                  // out: NetworkOPs, PeerImp
-JSS(ledger_current_index);        // out: NetworkOPs, RPCHelpers, LedgerCurrent, LedgerAccept,
-                                  //      AccountLines
-JSS(ledger_data);                 // out: LedgerHeader
-JSS(ledger_hash);                 // in: RPCHelpers, LedgerRequest, RipplePathFind,
-                                  //     TransactionEntry, handlers/Ledger
-                                  // out: NetworkOPs, RPCHelpers, LedgerClosed, LedgerData,
-                                  //      AccountLines
-JSS(ledger_hit_rate);             // out: GetCounts
-JSS(ledger_index);                // in/out: many
-JSS(ledger_index_max);            // in, out: AccountTx*
-JSS(ledger_index_min);            // in, out: AccountTx*
-JSS(ledger_max);                  // in, out: AccountTx*
-JSS(ledger_min);                  // in, out: AccountTx*
-JSS(ledger_time);                 // out: NetworkOPs
-JSS(LEDGER_ENTRY_TYPES);          // out: RPC server_definitions
-                                  // matches definitions.json format
-JSS(LEDGER_ENTRY_FLAGS);          // out: RPC server_definitions
-JSS(LEDGER_ENTRY_FORMATS);        // out: RPC server_definitions
-JSS(levels);                      // LogLevels
+JSS(confidential_balance_inbox);  // out: mpt_holders (confidential MPT)
+JSS(confidential_balance_spending);  // out: mpt_holders (confidential MPT)
+JSS(confidential_balance_version);   // out: mpt_holders (confidential MPT)
+JSS(consensus);                      // out: NetworkOPs, LedgerConsensus
+JSS(converge_time);                  // out: NetworkOPs
+JSS(converge_time_s);                // out: NetworkOPs
+JSS(cookie);                         // out: NetworkOPs
+JSS(count);                          // in: AccountTx*, ValidatorList
+JSS(counters);                       // in/out: retrieve counters
+JSS(credentials);                    // in: deposit_authorized
+JSS(credential_type);                // in: LedgerEntry DepositPreauth
+JSS(ctid);                           // in/out: Tx RPC
+JSS(currency_a);                     // out: BookChanges
+JSS(currency_b);                     // out: BookChanges
+JSS(currency);                       // in: paths/PathRequest, STAmount
+                                     // out: STPathSet, STAmount, AccountLines
+JSS(current);                        // out: OwnerInfo
+JSS(current_activities);             //
+JSS(current_ledger_size);            // out: TxQ
+JSS(current_queue_size);             // out: TxQ
+JSS(data);                           // out: LedgerData
+JSS(date);                           // out: tx/Transaction, NetworkOPs
+JSS(dbKBLedger);                     // out: getCounts
+JSS(dbKBTotal);                      // out: getCounts
+JSS(dbKBTransaction);                // out: getCounts
+JSS(debug_signing);                  // in: TransactionSign
+JSS(deletion_blockers_only);         // in: AccountObjects
+JSS(delivered_amount);               // out: insertDeliveredAmount
+JSS(deposit_authorized);             // out: deposit_authorized
+JSS(deprecated);                     //
+JSS(descending);                     // in: AccountTx*
+JSS(description);                    // in/out: Reservations
+JSS(destination);                    // in: nft_buy_offers, nft_sell_offers
+JSS(destination_account);            // in: PathRequest, RipplePathFind, account_lines
+                                     // out: AccountChannels
+JSS(destination_amount);             // in: PathRequest, RipplePathFind
+JSS(destination_currencies);         // in: PathRequest, RipplePathFind
+JSS(destination_tag);                // in: PathRequest
+                                     // out: AccountChannels
+JSS(details);                        // out: Manifest, server_info
+JSS(dir_entry);                      // out: DirectoryEntryIterator
+JSS(dir_index);                      // out: DirectoryEntryIterator
+JSS(dir_root);                       // out: DirectoryEntryIterator
+JSS(discounted_fee);                 // out: amm_info
+JSS(domain);                         // out: ValidatorInfo, Manifest
+JSS(drops);                          // out: TxQ
+JSS(duration_us);                    // out: NetworkOPs
+JSS(effective);                      // out: ValidatorList
+                                     // in: UNL
+JSS(enabled);                        // out: AmendmentTable
+JSS(engine_result);                  // out: NetworkOPs, TransactionSign, Submit
+JSS(engine_result_code);             // out: NetworkOPs, TransactionSign, Submit
+JSS(engine_result_message);          // out: NetworkOPs, TransactionSign, Submit
+JSS(entire_set);                     // out: get_aggregate_price
+JSS(ephemeral_key);                  // out: ValidatorInfo
+                                     // in/out: Manifest
+JSS(error);                          // out: error
+JSS(errored);                        //
+JSS(error_code);                     // out: error
+JSS(error_exception);                // out: Submit
+JSS(error_message);                  // out: error
+JSS(expand);                         // in: handler/Ledger
+JSS(expected_date);                  // out: any (warnings)
+JSS(expected_date_UTC);              // out: any (warnings)
+JSS(expected_ledger_size);           // out: TxQ
+JSS(expiration);                     // out: AccountOffers, AccountChannels, ValidatorList, amm_info
+JSS(fail_hard);                      // in: Sign, Submit
+JSS(failed);                         // out: InboundLedger
+JSS(feature);                        // in: Feature
+JSS(features);                       // out: Feature
+JSS(fee_base);                       // out: NetworkOPs
+JSS(fee_div_max);                    // in: TransactionSign
+JSS(fee_level);                      // out: AccountInfo
+JSS(fee_mult_max);                   // in: TransactionSign
+JSS(fee_ref);                        // out: NetworkOPs, DEPRECATED
+JSS(fetch_pack);                     // out: NetworkOPs
+JSS(FIELDS);                         // out: RPC server_definitions
+                                     // matches definitions.json format
+JSS(first);                          // out: rpc/Version
+JSS(finished);                       //
+JSS(fix_txns);                       // in: LedgerCleaner
+JSS(flags);                          // out: AccountOffers, NetworkOPs
+JSS(forward);                        // in: AccountTx
+JSS(freeze);                         // out: AccountLines
+JSS(freeze_peer);                    // out: AccountLines
+JSS(deep_freeze);                    // out: AccountLines
+JSS(deep_freeze_peer);               // out: AccountLines
+JSS(frozen_balances);                // out: GatewayBalances
+JSS(full);                           // in: LedgerClearer, handlers/Ledger
+JSS(full_reply);                     // out: PathFind
+JSS(fullbelow_size);                 // out: GetCounts
+JSS(git);                            // out: server_info
+JSS(good);                           // out: RPCVersion
+JSS(hash);                           // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field
+JSS(have_header);                    // out: InboundLedger
+JSS(have_state);                     // out: InboundLedger
+JSS(have_transactions);              // out: InboundLedger
+JSS(high);                           // out: BookChanges
+JSS(highest_sequence);               // out: AccountInfo
+JSS(highest_ticket);                 // out: AccountInfo
+JSS(historical_perminute);           // historical_perminute.
+JSS(holders);                        // out: MPTHolders
+JSS(holder_encryption_key);          // out: mpt_holders (confidential MPT)
+JSS(hostid);                         // out: NetworkOPs
+JSS(hotwallet);                      // in: GatewayBalances
+JSS(id);                             // websocket.
+JSS(ident);                          // in: AccountCurrencies, AccountInfo, OwnerInfo
+JSS(ignore_default);                 // in: AccountLines
+JSS(in);                             // out: OverlayImpl
+JSS(inLedger);                       // out: tx/Transaction
+JSS(inbound);                        // out: PeerImp
+JSS(index);                          // in: LedgerEntry
+                                     // out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData
+JSS(info);                           // out: ServerInfo, ConsensusInfo, FetchInfo
+JSS(initial_sync_duration_us);       //
+JSS(internal_command);               // in: Internal
+JSS(invalid_API_version);            // out: Many, when a request has an invalid version
+JSS(io_latency_ms);                  // out: NetworkOPs
+JSS(ip);                             // in: Connect, out: OverlayImpl
+JSS(is_burned);                      // out: nft_info (clio)
+JSS(isSerialized);                   // out: RPC server_definitions
+                                     // matches definitions.json format
+JSS(isSigningField);                 // out: RPC server_definitions
+                                     // matches definitions.json format
+JSS(isVLEncoded);                    // out: RPC server_definitions
+                                     // matches definitions.json format
+JSS(issuer);                         // in: RipplePathFind, Subscribe, Unsubscribe, BookOffers
+                                     // out: STPathSet, STAmount
+JSS(issuer_encrypted_balance);       // out: mpt_holders (confidential MPT)
+JSS(job);                            //
+JSS(job_queue);                      //
+JSS(jobs);                           //
+JSS(jsonrpc);                        // json version
+JSS(jq_trans_overflow);              // JobQueue transaction limit overflow.
+JSS(kept);                           // out: SubmitTransaction
+JSS(key);                            // out
+JSS(key_type);                       // in/out: WalletPropose, TransactionSign
+JSS(latency);                        // out: PeerImp
+JSS(last);                           // out: RPCVersion
+JSS(last_close);                     // out: NetworkOPs
+JSS(last_refresh_time);              // out: ValidatorSite
+JSS(last_refresh_status);            // out: ValidatorSite
+JSS(last_refresh_message);           // out: ValidatorSite
+JSS(ledger);                         // in: NetworkOPs, LedgerCleaner, RPCHelpers
+                                     // out: NetworkOPs, PeerImp
+JSS(ledger_current_index);           // out: NetworkOPs, RPCHelpers, LedgerCurrent, LedgerAccept,
+                                     //      AccountLines
+JSS(ledger_data);                    // out: LedgerHeader
+JSS(ledger_hash);                    // in: RPCHelpers, LedgerRequest, RipplePathFind,
+                                     //     TransactionEntry, handlers/Ledger
+                                     // out: NetworkOPs, RPCHelpers, LedgerClosed, LedgerData,
+                                     //      AccountLines
+JSS(ledger_hit_rate);                // out: GetCounts
+JSS(ledger_index);                   // in/out: many
+JSS(ledger_index_max);               // in, out: AccountTx*
+JSS(ledger_index_min);               // in, out: AccountTx*
+JSS(ledger_max);                     // in, out: AccountTx*
+JSS(ledger_min);                     // in, out: AccountTx*
+JSS(ledger_time);                    // out: NetworkOPs
+JSS(LEDGER_ENTRY_TYPES);             // out: RPC server_definitions
+                                     // matches definitions.json format
+JSS(LEDGER_ENTRY_FLAGS);             // out: RPC server_definitions
+JSS(LEDGER_ENTRY_FORMATS);           // out: RPC server_definitions
+JSS(levels);                         // LogLevels
 JSS(limit);                       // in/out: AccountTx*, AccountOffers, AccountLines, AccountObjects
                                   // in: LedgerData, BookOffers
 JSS(limit_peer);                  // out: AccountLines
@@ -401,6 +404,9 @@ JSS(min_ledger);                  // in: LedgerCleaner
 JSS(minimum_fee);                 // out: TxQ
 JSS(minimum_level);               // out: TxQ
 JSS(missingCommand);              // error
+JSS(mpt_amount);                  // out: mpt_holders
+JSS(mpt_issuance_id);             // in: Payment, mpt_holders
+JSS(mptoken_index);               // out: mpt_holders
 JSS(mpt_issuance_id_a);           // out: BookChanges
 JSS(mpt_issuance_id_b);           // out: BookChanges
 JSS(name);                        // out: AmendmentTableImpl, PeerImp
diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h
index 0d394020f7..379cfe53f5 100644
--- a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h
+++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h
@@ -147,6 +147,150 @@ public:
     {
         return this->sle_->at(sfPreviousTxnLgrSeq);
     }
+
+    /**
+     * @brief Get sfConfidentialBalanceInbox (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getConfidentialBalanceInbox() const
+    {
+        if (hasConfidentialBalanceInbox())
+            return this->sle_->at(sfConfidentialBalanceInbox);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfConfidentialBalanceInbox is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasConfidentialBalanceInbox() const
+    {
+        return this->sle_->isFieldPresent(sfConfidentialBalanceInbox);
+    }
+
+    /**
+     * @brief Get sfConfidentialBalanceSpending (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getConfidentialBalanceSpending() const
+    {
+        if (hasConfidentialBalanceSpending())
+            return this->sle_->at(sfConfidentialBalanceSpending);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfConfidentialBalanceSpending is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasConfidentialBalanceSpending() const
+    {
+        return this->sle_->isFieldPresent(sfConfidentialBalanceSpending);
+    }
+
+    /**
+     * @brief Get sfConfidentialBalanceVersion (SoeDefault)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getConfidentialBalanceVersion() const
+    {
+        if (hasConfidentialBalanceVersion())
+            return this->sle_->at(sfConfidentialBalanceVersion);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfConfidentialBalanceVersion is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasConfidentialBalanceVersion() const
+    {
+        return this->sle_->isFieldPresent(sfConfidentialBalanceVersion);
+    }
+
+    /**
+     * @brief Get sfIssuerEncryptedBalance (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getIssuerEncryptedBalance() const
+    {
+        if (hasIssuerEncryptedBalance())
+            return this->sle_->at(sfIssuerEncryptedBalance);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfIssuerEncryptedBalance is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasIssuerEncryptedBalance() const
+    {
+        return this->sle_->isFieldPresent(sfIssuerEncryptedBalance);
+    }
+
+    /**
+     * @brief Get sfAuditorEncryptedBalance (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getAuditorEncryptedBalance() const
+    {
+        if (hasAuditorEncryptedBalance())
+            return this->sle_->at(sfAuditorEncryptedBalance);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfAuditorEncryptedBalance is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasAuditorEncryptedBalance() const
+    {
+        return this->sle_->isFieldPresent(sfAuditorEncryptedBalance);
+    }
+
+    /**
+     * @brief Get sfHolderEncryptionKey (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getHolderEncryptionKey() const
+    {
+        if (hasHolderEncryptionKey())
+            return this->sle_->at(sfHolderEncryptionKey);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfHolderEncryptionKey is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasHolderEncryptionKey() const
+    {
+        return this->sle_->isFieldPresent(sfHolderEncryptionKey);
+    }
 };
 
 /**
@@ -270,6 +414,72 @@ public:
         return *this;
     }
 
+    /**
+     * @brief Set sfConfidentialBalanceInbox (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenBuilder&
+    setConfidentialBalanceInbox(std::decay_t const& value)
+    {
+        object_[sfConfidentialBalanceInbox] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfConfidentialBalanceSpending (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenBuilder&
+    setConfidentialBalanceSpending(std::decay_t const& value)
+    {
+        object_[sfConfidentialBalanceSpending] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfConfidentialBalanceVersion (SoeDefault)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenBuilder&
+    setConfidentialBalanceVersion(std::decay_t const& value)
+    {
+        object_[sfConfidentialBalanceVersion] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfIssuerEncryptedBalance (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenBuilder&
+    setIssuerEncryptedBalance(std::decay_t const& value)
+    {
+        object_[sfIssuerEncryptedBalance] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAuditorEncryptedBalance (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenBuilder&
+    setAuditorEncryptedBalance(std::decay_t const& value)
+    {
+        object_[sfAuditorEncryptedBalance] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfHolderEncryptionKey (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenBuilder&
+    setHolderEncryptionKey(std::decay_t const& value)
+    {
+        object_[sfHolderEncryptionKey] = value;
+        return *this;
+    }
+
     /**
      * @brief Build and return the completed MPToken wrapper.
      * @param index The ledger entry index.
diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h
index d493ac779c..b6c77093ac 100644
--- a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h
+++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h
@@ -302,6 +302,78 @@ public:
     {
         return this->sle_->isFieldPresent(sfReferenceHolding);
     }
+
+    /**
+     * @brief Get sfIssuerEncryptionKey (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getIssuerEncryptionKey() const
+    {
+        if (hasIssuerEncryptionKey())
+            return this->sle_->at(sfIssuerEncryptionKey);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfIssuerEncryptionKey is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasIssuerEncryptionKey() const
+    {
+        return this->sle_->isFieldPresent(sfIssuerEncryptionKey);
+    }
+
+    /**
+     * @brief Get sfAuditorEncryptionKey (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getAuditorEncryptionKey() const
+    {
+        if (hasAuditorEncryptionKey())
+            return this->sle_->at(sfAuditorEncryptionKey);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfAuditorEncryptionKey is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasAuditorEncryptionKey() const
+    {
+        return this->sle_->isFieldPresent(sfAuditorEncryptionKey);
+    }
+
+    /**
+     * @brief Get sfConfidentialOutstandingAmount (SoeDefault)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getConfidentialOutstandingAmount() const
+    {
+        if (hasConfidentialOutstandingAmount())
+            return this->sle_->at(sfConfidentialOutstandingAmount);
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfConfidentialOutstandingAmount is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasConfidentialOutstandingAmount() const
+    {
+        return this->sle_->isFieldPresent(sfConfidentialOutstandingAmount);
+    }
 };
 
 /**
@@ -504,6 +576,39 @@ public:
         return *this;
     }
 
+    /**
+     * @brief Set sfIssuerEncryptionKey (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenIssuanceBuilder&
+    setIssuerEncryptionKey(std::decay_t const& value)
+    {
+        object_[sfIssuerEncryptionKey] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAuditorEncryptionKey (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenIssuanceBuilder&
+    setAuditorEncryptionKey(std::decay_t const& value)
+    {
+        object_[sfAuditorEncryptionKey] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfConfidentialOutstandingAmount (SoeDefault)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenIssuanceBuilder&
+    setConfidentialOutstandingAmount(std::decay_t const& value)
+    {
+        object_[sfConfidentialOutstandingAmount] = value;
+        return *this;
+    }
+
     /**
      * @brief Build and return the completed MPTokenIssuance wrapper.
      * @param index The ledger entry index.
diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h
new file mode 100644
index 0000000000..2b16590649
--- /dev/null
+++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h
@@ -0,0 +1,201 @@
+// This file is auto-generated. Do not edit.
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::transactions {
+
+class ConfidentialMPTClawbackBuilder;
+
+/**
+ * @brief Transaction: ConfidentialMPTClawback
+ *
+ * Type: ttCONFIDENTIAL_MPT_CLAWBACK (89)
+ * Delegable: Delegation::Delegable
+ * Amendment: featureConfidentialTransfer
+ * Privileges: NoPriv
+ *
+ * Immutable wrapper around STTx providing type-safe field access.
+ * Use ConfidentialMPTClawbackBuilder to construct new transactions.
+ */
+class ConfidentialMPTClawback : public TransactionBase
+{
+public:
+    static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CLAWBACK;
+
+    /**
+     * @brief Construct a ConfidentialMPTClawback transaction wrapper from an existing STTx object.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    explicit ConfidentialMPTClawback(std::shared_ptr tx)
+        : TransactionBase(std::move(tx))
+    {
+        // Verify transaction type
+        if (tx_->getTxnType() != txType)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTClawback");
+        }
+    }
+
+    // Transaction-specific field getters
+
+    /**
+     * @brief Get sfMPTokenIssuanceID (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT192::type::value_type
+    getMPTokenIssuanceID() const
+    {
+        return this->tx_->at(sfMPTokenIssuanceID);
+    }
+
+    /**
+     * @brief Get sfHolder (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_ACCOUNT::type::value_type
+    getHolder() const
+    {
+        return this->tx_->at(sfHolder);
+    }
+
+    /**
+     * @brief Get sfMPTAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT64::type::value_type
+    getMPTAmount() const
+    {
+        return this->tx_->at(sfMPTAmount);
+    }
+
+    /**
+     * @brief Get sfZKProof (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getZKProof() const
+    {
+        return this->tx_->at(sfZKProof);
+    }
+};
+
+/**
+ * @brief Builder for ConfidentialMPTClawback transactions.
+ *
+ * Provides a fluent interface for constructing transactions with method chaining.
+ * Uses STObject internally for flexible transaction construction.
+ * Inherits common field setters from TransactionBuilderBase.
+ */
+class ConfidentialMPTClawbackBuilder : public TransactionBuilderBase
+{
+public:
+    /**
+     * @brief Construct a new ConfidentialMPTClawbackBuilder with required fields.
+     * @param account The account initiating the transaction.
+     * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
+     * @param holder The sfHolder field value.
+     * @param mPTAmount The sfMPTAmount field value.
+     * @param zKProof The sfZKProof field value.
+     * @param sequence Optional sequence number for the transaction.
+     * @param fee Optional fee for the transaction.
+     */
+    ConfidentialMPTClawbackBuilder(SF_ACCOUNT::type::value_type account,
+                     std::decay_t const& mPTokenIssuanceID,                     std::decay_t const& holder,                     std::decay_t const& mPTAmount,                     std::decay_t const& zKProof,                    std::optional sequence = std::nullopt,
+                    std::optional fee = std::nullopt
+)
+        : TransactionBuilderBase(ttCONFIDENTIAL_MPT_CLAWBACK, account, sequence, fee)
+    {
+        setMPTokenIssuanceID(mPTokenIssuanceID);
+        setHolder(holder);
+        setMPTAmount(mPTAmount);
+        setZKProof(zKProof);
+    }
+
+    /**
+     * @brief Construct a ConfidentialMPTClawbackBuilder from an existing STTx object.
+     * @param tx The existing transaction to copy from.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    ConfidentialMPTClawbackBuilder(std::shared_ptr tx)
+    {
+        if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CLAWBACK)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTClawbackBuilder");
+        }
+        object_ = *tx;
+    }
+
+    /** @brief Transaction-specific field setters */
+
+    /**
+     * @brief Set sfMPTokenIssuanceID (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTClawbackBuilder&
+    setMPTokenIssuanceID(std::decay_t const& value)
+    {
+        object_[sfMPTokenIssuanceID] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfHolder (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTClawbackBuilder&
+    setHolder(std::decay_t const& value)
+    {
+        object_[sfHolder] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfMPTAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTClawbackBuilder&
+    setMPTAmount(std::decay_t const& value)
+    {
+        object_[sfMPTAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfZKProof (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTClawbackBuilder&
+    setZKProof(std::decay_t const& value)
+    {
+        object_[sfZKProof] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Build and return the ConfidentialMPTClawback wrapper.
+     * @param publicKey The public key for signing.
+     * @param secretKey The secret key for signing.
+     * @return The constructed transaction wrapper.
+     */
+    ConfidentialMPTClawback
+    build(PublicKey const& publicKey, SecretKey const& secretKey)
+    {
+        sign(publicKey, secretKey);
+        return ConfidentialMPTClawback{std::make_shared(std::move(object_))};
+    }
+};
+
+}  // namespace xrpl::transactions
diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h
new file mode 100644
index 0000000000..f7a4cb601a
--- /dev/null
+++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h
@@ -0,0 +1,336 @@
+// This file is auto-generated. Do not edit.
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::transactions {
+
+class ConfidentialMPTConvertBuilder;
+
+/**
+ * @brief Transaction: ConfidentialMPTConvert
+ *
+ * Type: ttCONFIDENTIAL_MPT_CONVERT (85)
+ * Delegable: Delegation::Delegable
+ * Amendment: featureConfidentialTransfer
+ * Privileges: NoPriv
+ *
+ * Immutable wrapper around STTx providing type-safe field access.
+ * Use ConfidentialMPTConvertBuilder to construct new transactions.
+ */
+class ConfidentialMPTConvert : public TransactionBase
+{
+public:
+    static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CONVERT;
+
+    /**
+     * @brief Construct a ConfidentialMPTConvert transaction wrapper from an existing STTx object.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    explicit ConfidentialMPTConvert(std::shared_ptr tx)
+        : TransactionBase(std::move(tx))
+    {
+        // Verify transaction type
+        if (tx_->getTxnType() != txType)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvert");
+        }
+    }
+
+    // Transaction-specific field getters
+
+    /**
+     * @brief Get sfMPTokenIssuanceID (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT192::type::value_type
+    getMPTokenIssuanceID() const
+    {
+        return this->tx_->at(sfMPTokenIssuanceID);
+    }
+
+    /**
+     * @brief Get sfMPTAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT64::type::value_type
+    getMPTAmount() const
+    {
+        return this->tx_->at(sfMPTAmount);
+    }
+
+    /**
+     * @brief Get sfHolderEncryptionKey (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getHolderEncryptionKey() const
+    {
+        if (hasHolderEncryptionKey())
+        {
+            return this->tx_->at(sfHolderEncryptionKey);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfHolderEncryptionKey is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasHolderEncryptionKey() const
+    {
+        return this->tx_->isFieldPresent(sfHolderEncryptionKey);
+    }
+
+    /**
+     * @brief Get sfHolderEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getHolderEncryptedAmount() const
+    {
+        return this->tx_->at(sfHolderEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfIssuerEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getIssuerEncryptedAmount() const
+    {
+        return this->tx_->at(sfIssuerEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfAuditorEncryptedAmount (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getAuditorEncryptedAmount() const
+    {
+        if (hasAuditorEncryptedAmount())
+        {
+            return this->tx_->at(sfAuditorEncryptedAmount);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfAuditorEncryptedAmount is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasAuditorEncryptedAmount() const
+    {
+        return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfBlindingFactor (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT256::type::value_type
+    getBlindingFactor() const
+    {
+        return this->tx_->at(sfBlindingFactor);
+    }
+
+    /**
+     * @brief Get sfZKProof (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getZKProof() const
+    {
+        if (hasZKProof())
+        {
+            return this->tx_->at(sfZKProof);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfZKProof is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasZKProof() const
+    {
+        return this->tx_->isFieldPresent(sfZKProof);
+    }
+};
+
+/**
+ * @brief Builder for ConfidentialMPTConvert transactions.
+ *
+ * Provides a fluent interface for constructing transactions with method chaining.
+ * Uses STObject internally for flexible transaction construction.
+ * Inherits common field setters from TransactionBuilderBase.
+ */
+class ConfidentialMPTConvertBuilder : public TransactionBuilderBase
+{
+public:
+    /**
+     * @brief Construct a new ConfidentialMPTConvertBuilder with required fields.
+     * @param account The account initiating the transaction.
+     * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
+     * @param mPTAmount The sfMPTAmount field value.
+     * @param holderEncryptedAmount The sfHolderEncryptedAmount field value.
+     * @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value.
+     * @param blindingFactor The sfBlindingFactor field value.
+     * @param sequence Optional sequence number for the transaction.
+     * @param fee Optional fee for the transaction.
+     */
+    ConfidentialMPTConvertBuilder(SF_ACCOUNT::type::value_type account,
+                     std::decay_t const& mPTokenIssuanceID,                     std::decay_t const& mPTAmount,                     std::decay_t const& holderEncryptedAmount,                     std::decay_t const& issuerEncryptedAmount,                     std::decay_t const& blindingFactor,                    std::optional sequence = std::nullopt,
+                    std::optional fee = std::nullopt
+)
+        : TransactionBuilderBase(ttCONFIDENTIAL_MPT_CONVERT, account, sequence, fee)
+    {
+        setMPTokenIssuanceID(mPTokenIssuanceID);
+        setMPTAmount(mPTAmount);
+        setHolderEncryptedAmount(holderEncryptedAmount);
+        setIssuerEncryptedAmount(issuerEncryptedAmount);
+        setBlindingFactor(blindingFactor);
+    }
+
+    /**
+     * @brief Construct a ConfidentialMPTConvertBuilder from an existing STTx object.
+     * @param tx The existing transaction to copy from.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    ConfidentialMPTConvertBuilder(std::shared_ptr tx)
+    {
+        if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CONVERT)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBuilder");
+        }
+        object_ = *tx;
+    }
+
+    /** @brief Transaction-specific field setters */
+
+    /**
+     * @brief Set sfMPTokenIssuanceID (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setMPTokenIssuanceID(std::decay_t const& value)
+    {
+        object_[sfMPTokenIssuanceID] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfMPTAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setMPTAmount(std::decay_t const& value)
+    {
+        object_[sfMPTAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfHolderEncryptionKey (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setHolderEncryptionKey(std::decay_t const& value)
+    {
+        object_[sfHolderEncryptionKey] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfHolderEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setHolderEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfHolderEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfIssuerEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setIssuerEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfIssuerEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAuditorEncryptedAmount (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setAuditorEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfAuditorEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfBlindingFactor (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setBlindingFactor(std::decay_t const& value)
+    {
+        object_[sfBlindingFactor] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfZKProof (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBuilder&
+    setZKProof(std::decay_t const& value)
+    {
+        object_[sfZKProof] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Build and return the ConfidentialMPTConvert wrapper.
+     * @param publicKey The public key for signing.
+     * @param secretKey The secret key for signing.
+     * @return The constructed transaction wrapper.
+     */
+    ConfidentialMPTConvert
+    build(PublicKey const& publicKey, SecretKey const& secretKey)
+    {
+        sign(publicKey, secretKey);
+        return ConfidentialMPTConvert{std::make_shared(std::move(object_))};
+    }
+};
+
+}  // namespace xrpl::transactions
diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h
new file mode 100644
index 0000000000..68bf326645
--- /dev/null
+++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h
@@ -0,0 +1,310 @@
+// This file is auto-generated. Do not edit.
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::transactions {
+
+class ConfidentialMPTConvertBackBuilder;
+
+/**
+ * @brief Transaction: ConfidentialMPTConvertBack
+ *
+ * Type: ttCONFIDENTIAL_MPT_CONVERT_BACK (87)
+ * Delegable: Delegation::Delegable
+ * Amendment: featureConfidentialTransfer
+ * Privileges: NoPriv
+ *
+ * Immutable wrapper around STTx providing type-safe field access.
+ * Use ConfidentialMPTConvertBackBuilder to construct new transactions.
+ */
+class ConfidentialMPTConvertBack : public TransactionBase
+{
+public:
+    static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CONVERT_BACK;
+
+    /**
+     * @brief Construct a ConfidentialMPTConvertBack transaction wrapper from an existing STTx object.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    explicit ConfidentialMPTConvertBack(std::shared_ptr tx)
+        : TransactionBase(std::move(tx))
+    {
+        // Verify transaction type
+        if (tx_->getTxnType() != txType)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBack");
+        }
+    }
+
+    // Transaction-specific field getters
+
+    /**
+     * @brief Get sfMPTokenIssuanceID (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT192::type::value_type
+    getMPTokenIssuanceID() const
+    {
+        return this->tx_->at(sfMPTokenIssuanceID);
+    }
+
+    /**
+     * @brief Get sfMPTAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT64::type::value_type
+    getMPTAmount() const
+    {
+        return this->tx_->at(sfMPTAmount);
+    }
+
+    /**
+     * @brief Get sfHolderEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getHolderEncryptedAmount() const
+    {
+        return this->tx_->at(sfHolderEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfIssuerEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getIssuerEncryptedAmount() const
+    {
+        return this->tx_->at(sfIssuerEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfAuditorEncryptedAmount (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getAuditorEncryptedAmount() const
+    {
+        if (hasAuditorEncryptedAmount())
+        {
+            return this->tx_->at(sfAuditorEncryptedAmount);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfAuditorEncryptedAmount is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasAuditorEncryptedAmount() const
+    {
+        return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfBlindingFactor (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT256::type::value_type
+    getBlindingFactor() const
+    {
+        return this->tx_->at(sfBlindingFactor);
+    }
+
+    /**
+     * @brief Get sfZKProof (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getZKProof() const
+    {
+        return this->tx_->at(sfZKProof);
+    }
+
+    /**
+     * @brief Get sfBalanceCommitment (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getBalanceCommitment() const
+    {
+        return this->tx_->at(sfBalanceCommitment);
+    }
+};
+
+/**
+ * @brief Builder for ConfidentialMPTConvertBack transactions.
+ *
+ * Provides a fluent interface for constructing transactions with method chaining.
+ * Uses STObject internally for flexible transaction construction.
+ * Inherits common field setters from TransactionBuilderBase.
+ */
+class ConfidentialMPTConvertBackBuilder : public TransactionBuilderBase
+{
+public:
+    /**
+     * @brief Construct a new ConfidentialMPTConvertBackBuilder with required fields.
+     * @param account The account initiating the transaction.
+     * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
+     * @param mPTAmount The sfMPTAmount field value.
+     * @param holderEncryptedAmount The sfHolderEncryptedAmount field value.
+     * @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value.
+     * @param blindingFactor The sfBlindingFactor field value.
+     * @param zKProof The sfZKProof field value.
+     * @param balanceCommitment The sfBalanceCommitment field value.
+     * @param sequence Optional sequence number for the transaction.
+     * @param fee Optional fee for the transaction.
+     */
+    ConfidentialMPTConvertBackBuilder(SF_ACCOUNT::type::value_type account,
+                     std::decay_t const& mPTokenIssuanceID,                     std::decay_t const& mPTAmount,                     std::decay_t const& holderEncryptedAmount,                     std::decay_t const& issuerEncryptedAmount,                     std::decay_t const& blindingFactor,                     std::decay_t const& zKProof,                     std::decay_t const& balanceCommitment,                    std::optional sequence = std::nullopt,
+                    std::optional fee = std::nullopt
+)
+        : TransactionBuilderBase(ttCONFIDENTIAL_MPT_CONVERT_BACK, account, sequence, fee)
+    {
+        setMPTokenIssuanceID(mPTokenIssuanceID);
+        setMPTAmount(mPTAmount);
+        setHolderEncryptedAmount(holderEncryptedAmount);
+        setIssuerEncryptedAmount(issuerEncryptedAmount);
+        setBlindingFactor(blindingFactor);
+        setZKProof(zKProof);
+        setBalanceCommitment(balanceCommitment);
+    }
+
+    /**
+     * @brief Construct a ConfidentialMPTConvertBackBuilder from an existing STTx object.
+     * @param tx The existing transaction to copy from.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    ConfidentialMPTConvertBackBuilder(std::shared_ptr tx)
+    {
+        if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CONVERT_BACK)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBackBuilder");
+        }
+        object_ = *tx;
+    }
+
+    /** @brief Transaction-specific field setters */
+
+    /**
+     * @brief Set sfMPTokenIssuanceID (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setMPTokenIssuanceID(std::decay_t const& value)
+    {
+        object_[sfMPTokenIssuanceID] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfMPTAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setMPTAmount(std::decay_t const& value)
+    {
+        object_[sfMPTAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfHolderEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setHolderEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfHolderEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfIssuerEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setIssuerEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfIssuerEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAuditorEncryptedAmount (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setAuditorEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfAuditorEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfBlindingFactor (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setBlindingFactor(std::decay_t const& value)
+    {
+        object_[sfBlindingFactor] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfZKProof (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setZKProof(std::decay_t const& value)
+    {
+        object_[sfZKProof] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfBalanceCommitment (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTConvertBackBuilder&
+    setBalanceCommitment(std::decay_t const& value)
+    {
+        object_[sfBalanceCommitment] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Build and return the ConfidentialMPTConvertBack wrapper.
+     * @param publicKey The public key for signing.
+     * @param secretKey The secret key for signing.
+     * @return The constructed transaction wrapper.
+     */
+    ConfidentialMPTConvertBack
+    build(PublicKey const& publicKey, SecretKey const& secretKey)
+    {
+        sign(publicKey, secretKey);
+        return ConfidentialMPTConvertBack{std::make_shared(std::move(object_))};
+    }
+};
+
+}  // namespace xrpl::transactions
diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h
new file mode 100644
index 0000000000..bb932080d8
--- /dev/null
+++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h
@@ -0,0 +1,129 @@
+// This file is auto-generated. Do not edit.
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::transactions {
+
+class ConfidentialMPTMergeInboxBuilder;
+
+/**
+ * @brief Transaction: ConfidentialMPTMergeInbox
+ *
+ * Type: ttCONFIDENTIAL_MPT_MERGE_INBOX (86)
+ * Delegable: Delegation::Delegable
+ * Amendment: featureConfidentialTransfer
+ * Privileges: NoPriv
+ *
+ * Immutable wrapper around STTx providing type-safe field access.
+ * Use ConfidentialMPTMergeInboxBuilder to construct new transactions.
+ */
+class ConfidentialMPTMergeInbox : public TransactionBase
+{
+public:
+    static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_MERGE_INBOX;
+
+    /**
+     * @brief Construct a ConfidentialMPTMergeInbox transaction wrapper from an existing STTx object.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    explicit ConfidentialMPTMergeInbox(std::shared_ptr tx)
+        : TransactionBase(std::move(tx))
+    {
+        // Verify transaction type
+        if (tx_->getTxnType() != txType)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTMergeInbox");
+        }
+    }
+
+    // Transaction-specific field getters
+
+    /**
+     * @brief Get sfMPTokenIssuanceID (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT192::type::value_type
+    getMPTokenIssuanceID() const
+    {
+        return this->tx_->at(sfMPTokenIssuanceID);
+    }
+};
+
+/**
+ * @brief Builder for ConfidentialMPTMergeInbox transactions.
+ *
+ * Provides a fluent interface for constructing transactions with method chaining.
+ * Uses STObject internally for flexible transaction construction.
+ * Inherits common field setters from TransactionBuilderBase.
+ */
+class ConfidentialMPTMergeInboxBuilder : public TransactionBuilderBase
+{
+public:
+    /**
+     * @brief Construct a new ConfidentialMPTMergeInboxBuilder with required fields.
+     * @param account The account initiating the transaction.
+     * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
+     * @param sequence Optional sequence number for the transaction.
+     * @param fee Optional fee for the transaction.
+     */
+    ConfidentialMPTMergeInboxBuilder(SF_ACCOUNT::type::value_type account,
+                     std::decay_t const& mPTokenIssuanceID,                    std::optional sequence = std::nullopt,
+                    std::optional fee = std::nullopt
+)
+        : TransactionBuilderBase(ttCONFIDENTIAL_MPT_MERGE_INBOX, account, sequence, fee)
+    {
+        setMPTokenIssuanceID(mPTokenIssuanceID);
+    }
+
+    /**
+     * @brief Construct a ConfidentialMPTMergeInboxBuilder from an existing STTx object.
+     * @param tx The existing transaction to copy from.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    ConfidentialMPTMergeInboxBuilder(std::shared_ptr tx)
+    {
+        if (tx->getTxnType() != ttCONFIDENTIAL_MPT_MERGE_INBOX)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTMergeInboxBuilder");
+        }
+        object_ = *tx;
+    }
+
+    /** @brief Transaction-specific field setters */
+
+    /**
+     * @brief Set sfMPTokenIssuanceID (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTMergeInboxBuilder&
+    setMPTokenIssuanceID(std::decay_t const& value)
+    {
+        object_[sfMPTokenIssuanceID] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Build and return the ConfidentialMPTMergeInbox wrapper.
+     * @param publicKey The public key for signing.
+     * @param secretKey The secret key for signing.
+     * @return The constructed transaction wrapper.
+     */
+    ConfidentialMPTMergeInbox
+    build(PublicKey const& publicKey, SecretKey const& secretKey)
+    {
+        sign(publicKey, secretKey);
+        return ConfidentialMPTMergeInbox{std::make_shared(std::move(object_))};
+    }
+};
+
+}  // namespace xrpl::transactions
diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h
new file mode 100644
index 0000000000..2d8a77d56f
--- /dev/null
+++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h
@@ -0,0 +1,408 @@
+// This file is auto-generated. Do not edit.
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::transactions {
+
+class ConfidentialMPTSendBuilder;
+
+/**
+ * @brief Transaction: ConfidentialMPTSend
+ *
+ * Type: ttCONFIDENTIAL_MPT_SEND (88)
+ * Delegable: Delegation::Delegable
+ * Amendment: featureConfidentialTransfer
+ * Privileges: NoPriv
+ *
+ * Immutable wrapper around STTx providing type-safe field access.
+ * Use ConfidentialMPTSendBuilder to construct new transactions.
+ */
+class ConfidentialMPTSend : public TransactionBase
+{
+public:
+    static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_SEND;
+
+    /**
+     * @brief Construct a ConfidentialMPTSend transaction wrapper from an existing STTx object.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    explicit ConfidentialMPTSend(std::shared_ptr tx)
+        : TransactionBase(std::move(tx))
+    {
+        // Verify transaction type
+        if (tx_->getTxnType() != txType)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTSend");
+        }
+    }
+
+    // Transaction-specific field getters
+
+    /**
+     * @brief Get sfMPTokenIssuanceID (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_UINT192::type::value_type
+    getMPTokenIssuanceID() const
+    {
+        return this->tx_->at(sfMPTokenIssuanceID);
+    }
+
+    /**
+     * @brief Get sfDestination (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_ACCOUNT::type::value_type
+    getDestination() const
+    {
+        return this->tx_->at(sfDestination);
+    }
+
+    /**
+     * @brief Get sfDestinationTag (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getDestinationTag() const
+    {
+        if (hasDestinationTag())
+        {
+            return this->tx_->at(sfDestinationTag);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfDestinationTag is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasDestinationTag() const
+    {
+        return this->tx_->isFieldPresent(sfDestinationTag);
+    }
+
+    /**
+     * @brief Get sfSenderEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getSenderEncryptedAmount() const
+    {
+        return this->tx_->at(sfSenderEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfDestinationEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getDestinationEncryptedAmount() const
+    {
+        return this->tx_->at(sfDestinationEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfIssuerEncryptedAmount (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getIssuerEncryptedAmount() const
+    {
+        return this->tx_->at(sfIssuerEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfAuditorEncryptedAmount (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getAuditorEncryptedAmount() const
+    {
+        if (hasAuditorEncryptedAmount())
+        {
+            return this->tx_->at(sfAuditorEncryptedAmount);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfAuditorEncryptedAmount is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasAuditorEncryptedAmount() const
+    {
+        return this->tx_->isFieldPresent(sfAuditorEncryptedAmount);
+    }
+
+    /**
+     * @brief Get sfZKProof (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getZKProof() const
+    {
+        return this->tx_->at(sfZKProof);
+    }
+
+    /**
+     * @brief Get sfAmountCommitment (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getAmountCommitment() const
+    {
+        return this->tx_->at(sfAmountCommitment);
+    }
+
+    /**
+     * @brief Get sfBalanceCommitment (SoeRequired)
+     * @return The field value.
+     */
+    [[nodiscard]]
+    SF_VL::type::value_type
+    getBalanceCommitment() const
+    {
+        return this->tx_->at(sfBalanceCommitment);
+    }
+
+    /**
+     * @brief Get sfCredentialIDs (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getCredentialIDs() const
+    {
+        if (hasCredentialIDs())
+        {
+            return this->tx_->at(sfCredentialIDs);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfCredentialIDs is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasCredentialIDs() const
+    {
+        return this->tx_->isFieldPresent(sfCredentialIDs);
+    }
+};
+
+/**
+ * @brief Builder for ConfidentialMPTSend transactions.
+ *
+ * Provides a fluent interface for constructing transactions with method chaining.
+ * Uses STObject internally for flexible transaction construction.
+ * Inherits common field setters from TransactionBuilderBase.
+ */
+class ConfidentialMPTSendBuilder : public TransactionBuilderBase
+{
+public:
+    /**
+     * @brief Construct a new ConfidentialMPTSendBuilder with required fields.
+     * @param account The account initiating the transaction.
+     * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value.
+     * @param destination The sfDestination field value.
+     * @param senderEncryptedAmount The sfSenderEncryptedAmount field value.
+     * @param destinationEncryptedAmount The sfDestinationEncryptedAmount field value.
+     * @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value.
+     * @param zKProof The sfZKProof field value.
+     * @param amountCommitment The sfAmountCommitment field value.
+     * @param balanceCommitment The sfBalanceCommitment field value.
+     * @param sequence Optional sequence number for the transaction.
+     * @param fee Optional fee for the transaction.
+     */
+    ConfidentialMPTSendBuilder(SF_ACCOUNT::type::value_type account,
+                     std::decay_t const& mPTokenIssuanceID,                     std::decay_t const& destination,                     std::decay_t const& senderEncryptedAmount,                     std::decay_t const& destinationEncryptedAmount,                     std::decay_t const& issuerEncryptedAmount,                     std::decay_t const& zKProof,                     std::decay_t const& amountCommitment,                     std::decay_t const& balanceCommitment,                    std::optional sequence = std::nullopt,
+                    std::optional fee = std::nullopt
+)
+        : TransactionBuilderBase(ttCONFIDENTIAL_MPT_SEND, account, sequence, fee)
+    {
+        setMPTokenIssuanceID(mPTokenIssuanceID);
+        setDestination(destination);
+        setSenderEncryptedAmount(senderEncryptedAmount);
+        setDestinationEncryptedAmount(destinationEncryptedAmount);
+        setIssuerEncryptedAmount(issuerEncryptedAmount);
+        setZKProof(zKProof);
+        setAmountCommitment(amountCommitment);
+        setBalanceCommitment(balanceCommitment);
+    }
+
+    /**
+     * @brief Construct a ConfidentialMPTSendBuilder from an existing STTx object.
+     * @param tx The existing transaction to copy from.
+     * @throws std::runtime_error if the transaction type doesn't match.
+     */
+    ConfidentialMPTSendBuilder(std::shared_ptr tx)
+    {
+        if (tx->getTxnType() != ttCONFIDENTIAL_MPT_SEND)
+        {
+            throw std::runtime_error("Invalid transaction type for ConfidentialMPTSendBuilder");
+        }
+        object_ = *tx;
+    }
+
+    /** @brief Transaction-specific field setters */
+
+    /**
+     * @brief Set sfMPTokenIssuanceID (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setMPTokenIssuanceID(std::decay_t const& value)
+    {
+        object_[sfMPTokenIssuanceID] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfDestination (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setDestination(std::decay_t const& value)
+    {
+        object_[sfDestination] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfDestinationTag (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setDestinationTag(std::decay_t const& value)
+    {
+        object_[sfDestinationTag] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfSenderEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setSenderEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfSenderEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfDestinationEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setDestinationEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfDestinationEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfIssuerEncryptedAmount (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setIssuerEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfIssuerEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAuditorEncryptedAmount (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setAuditorEncryptedAmount(std::decay_t const& value)
+    {
+        object_[sfAuditorEncryptedAmount] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfZKProof (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setZKProof(std::decay_t const& value)
+    {
+        object_[sfZKProof] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAmountCommitment (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setAmountCommitment(std::decay_t const& value)
+    {
+        object_[sfAmountCommitment] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfBalanceCommitment (SoeRequired)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setBalanceCommitment(std::decay_t const& value)
+    {
+        object_[sfBalanceCommitment] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfCredentialIDs (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    ConfidentialMPTSendBuilder&
+    setCredentialIDs(std::decay_t const& value)
+    {
+        object_[sfCredentialIDs] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Build and return the ConfidentialMPTSend wrapper.
+     * @param publicKey The public key for signing.
+     * @param secretKey The secret key for signing.
+     * @return The constructed transaction wrapper.
+     */
+    ConfidentialMPTSend
+    build(PublicKey const& publicKey, SecretKey const& secretKey)
+    {
+        sign(publicKey, secretKey);
+        return ConfidentialMPTSend{std::make_shared(std::move(object_))};
+    }
+};
+
+}  // namespace xrpl::transactions
diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h
index a92521173b..8099af1148 100644
--- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h
+++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h
@@ -187,6 +187,58 @@ public:
     {
         return this->tx_->isFieldPresent(sfMutableFlags);
     }
+
+    /**
+     * @brief Get sfIssuerEncryptionKey (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getIssuerEncryptionKey() const
+    {
+        if (hasIssuerEncryptionKey())
+        {
+            return this->tx_->at(sfIssuerEncryptionKey);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfIssuerEncryptionKey is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasIssuerEncryptionKey() const
+    {
+        return this->tx_->isFieldPresent(sfIssuerEncryptionKey);
+    }
+
+    /**
+     * @brief Get sfAuditorEncryptionKey (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getAuditorEncryptionKey() const
+    {
+        if (hasAuditorEncryptionKey())
+        {
+            return this->tx_->at(sfAuditorEncryptionKey);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfAuditorEncryptionKey is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasAuditorEncryptionKey() const
+    {
+        return this->tx_->isFieldPresent(sfAuditorEncryptionKey);
+    }
 };
 
 /**
@@ -297,6 +349,28 @@ public:
         return *this;
     }
 
+    /**
+     * @brief Set sfIssuerEncryptionKey (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenIssuanceSetBuilder&
+    setIssuerEncryptionKey(std::decay_t const& value)
+    {
+        object_[sfIssuerEncryptionKey] = value;
+        return *this;
+    }
+
+    /**
+     * @brief Set sfAuditorEncryptionKey (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    MPTokenIssuanceSetBuilder&
+    setAuditorEncryptionKey(std::decay_t const& value)
+    {
+        object_[sfAuditorEncryptionKey] = value;
+        return *this;
+    }
+
     /**
      * @brief Build and return the MPTokenIssuanceSet wrapper.
      * @param publicKey The public key for signing.
diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h
index 470571eb48..02fedfe970 100644
--- a/include/xrpl/tx/Transactor.h
+++ b/include/xrpl/tx/Transactor.h
@@ -7,6 +7,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
@@ -186,6 +187,10 @@ public:
     static XRPAmount
     calculateBaseFee(ReadView const& view, STTx const& tx);
 
+    // Returns the base fee plus extra base fee units, not scaled for load.
+    static XRPAmount
+    calculateBaseFee(ReadView const& view, STTx const& tx, std::uint32_t extraBaseFeeMultiplier);
+
     /* Do NOT define an invokePreflight function in a derived class.
        Instead, define:
 
diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h
index 9378062726..8931d189fd 100644
--- a/include/xrpl/tx/invariants/InvariantCheck.h
+++ b/include/xrpl/tx/invariants/InvariantCheck.h
@@ -413,6 +413,7 @@ using InvariantChecks = std::tuple<
     ValidLoanBroker,
     ValidLoan,
     ValidVault,
+    ValidConfidentialMPToken,
     ValidMPTPayment,
     ValidAmounts,
     ValidMPTTransfer>;
diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h
index b4b76a290f..aa90f1b8ef 100644
--- a/include/xrpl/tx/invariants/MPTInvariant.h
+++ b/include/xrpl/tx/invariants/MPTInvariant.h
@@ -36,17 +36,42 @@ class ValidMPTIssuance
     std::vector> deletedHoldings_;
 
 public:
+    /**
+     * @brief Track MPT issuance and holding creations, deletions, and
+     * mutations.
+     *
+     * @param isDelete Whether the ledger entry is being deleted.
+     * @param before The ledger entry before transaction application.
+     * @param after The ledger entry after transaction application.
+     */
     void
-    visitEntry(bool, SLE::const_ref, SLE::const_ref);
+    visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
 
+    /**
+     * @brief Verify MPT issuance invariants after transaction application.
+     *
+     * @param tx The transaction being checked.
+     * @param result The transaction result code.
+     * @param fee The fee charged by the transaction.
+     * @param view The ledger view after transaction application.
+     * @param j Journal used for diagnostics.
+     * @return true if the invariant checks pass, otherwise false.
+     */
     [[nodiscard]] bool
-    finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
+    finalize(
+        STTx const& tx,
+        TER const result,
+        XRPAmount const fee,
+        ReadView const& view,
+        beast::Journal const& j) const;
 };
 
-/** Verify:
- *    - OutstandingAmount <= MaximumAmount for any MPT
- *    - OutstandingAmount after = OutstandingAmount before +
- *         sum (MPT after - MPT before) - this is total MPT credit/debit
+/**
+ * @brief Verify public MPT amount and outstanding amount accounting.
+ *
+ * Checks that OutstandingAmount does not exceed MaximumAmount and that
+ * OutstandingAmount after application equals OutstandingAmount before
+ * application plus the net holder balance delta.
  */
 class ValidMPTPayment
 {
@@ -64,11 +89,104 @@ class ValidMPTPayment
     hash_map data_;
 
 public:
+    /**
+     * @brief Track MPT amount and outstanding amount changes.
+     *
+     * @param isDelete Whether the ledger entry is being deleted.
+     * @param before The ledger entry before transaction application.
+     * @param after The ledger entry after transaction application.
+     */
     void
-    visitEntry(bool, SLE::const_ref, SLE::const_ref);
+    visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
 
+    /**
+     * @brief Verify public MPT payment accounting invariants.
+     *
+     * @param tx The transaction being checked.
+     * @param result The transaction result code.
+     * @param fee The fee charged by the transaction.
+     * @param view The ledger view after transaction application.
+     * @param j Journal used for diagnostics.
+     * @return true if the invariant checks pass, otherwise false.
+     */
     bool
-    finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
+    finalize(
+        STTx const& tx,
+        TER const result,
+        XRPAmount const fee,
+        ReadView const& view,
+        beast::Journal const& j);
+};
+
+/**
+ * @brief Invariants: Confidential MPToken consistency
+ *
+ * - Convert/ConvertBack symmetry:
+ * Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
+ * - Cannot delete MPToken with non-zero confidential state:
+ * Cannot delete if sfIssuerEncryptedBalance exists
+ * Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
+ * - Privacy flag consistency:
+ * MPToken confidential balance fields can only be created or changed if
+ * lsfMPTCanHoldConfidentialBalance is set on the issuance.
+ * - Encrypted field existence consistency:
+ * If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then
+ * sfIssuerEncryptedBalance must also exist (and vice versa). If
+ * sfAuditorEncryptedBalance exists, then those core encrypted balance fields
+ * must also exist.
+ * - COA <= OutstandingAmount:
+ * Confidential outstanding balance cannot exceed total outstanding.
+ * - Verifies sfConfidentialBalanceVersion is changed whenever sfConfidentialBalanceSpending is
+ * modified on an MPToken.
+ */
+class ValidConfidentialMPToken
+{
+    struct Changes
+    {
+        std::int64_t mptAmountDelta = 0;
+        std::int64_t coaDelta = 0;
+        std::int64_t outstandingDelta = 0;
+        SLE::const_pointer issuance;
+        bool deletedWithEncrypted = false;
+        bool badConsistency = false;
+        bool badCOA = false;
+        bool changesConfidentialFields = false;
+        bool badVersion = false;
+    };
+    std::map changes_;
+
+public:
+    /**
+     * @brief Track confidential MPT balance, issuance, and version changes.
+     *
+     * @param isDelete Whether the ledger entry is being deleted.
+     * @param before The ledger entry before transaction application.
+     * @param after The ledger entry after transaction application.
+     */
+    void
+    visitEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after);
+
+    /**
+     * @brief Verify confidential MPT accounting and encrypted-field
+     * invariants.
+     *
+     * @param tx The transaction being checked.
+     * @param result The transaction result code.
+     * @param fee The fee charged by the transaction.
+     * @param view The ledger view after transaction application.
+     * @param j Journal used for diagnostics.
+     * @return true if the invariant checks pass, otherwise false.
+     */
+    bool
+    finalize(
+        STTx const& tx,
+        TER const result,
+        XRPAmount const fee,
+        ReadView const& view,
+        beast::Journal const& j);
 };
 
 class ValidMPTTransfer
@@ -85,11 +203,36 @@ class ValidMPTTransfer
     hash_map deletedAuthorized_;
 
 public:
+    /**
+     * @brief Track MPT balance changes and deleted authorization state.
+     *
+     * @param isDelete Whether the ledger entry is being deleted.
+     * @param before The ledger entry before transaction application.
+     * @param after The ledger entry after transaction application.
+     */
     void
-    visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&);
+    visitEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after);
 
+    /**
+     * @brief Verify MPT transfer authorization invariants.
+     *
+     * @param tx The transaction being checked.
+     * @param result The transaction result code.
+     * @param fee The fee charged by the transaction.
+     * @param view The ledger view after transaction application.
+     * @param j Journal used for diagnostics.
+     * @return true if the invariant checks pass, otherwise false.
+     */
     bool
-    finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
+    finalize(
+        STTx const& tx,
+        TER const result,
+        XRPAmount const fee,
+        ReadView const& view,
+        beast::Journal const& j);
 
 private:
     /**
@@ -99,7 +242,13 @@ private:
      * finalize() runs, so their authorization state is captured during
      * visitEntry() and stored in deletedAuthorized_. For deleted MPTokens,
      * returns true if reqAuth is false or lsfMPTAuthorized was set at deletion.
-     * For existing MPTokens, returns the result of requireAuth()
+     * For existing MPTokens, returns the result of requireAuth().
+     *
+     * @param view The ledger view after transaction application.
+     * @param mptid The MPToken issuance ID.
+     * @param holder The holder account being checked.
+     * @param requireAuth Whether the issuance requires explicit authorization.
+     * @return true if the holder is authorized, otherwise false.
      */
     [[nodiscard]] bool
     isAuthorized(
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h
new file mode 100644
index 0000000000..ff6f76e8ea
--- /dev/null
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include 
+
+namespace xrpl {
+
+/**
+ * @brief Allows an MPT issuer to clawback confidential balances from a holder.
+ *
+ * This transaction enables the issuer of an MPToken Issuance (with clawback
+ * enabled) to reclaim confidential tokens from a holder's account. Unlike
+ * regular clawback, the issuer cannot see the holder's balance directly.
+ * Instead, the issuer must provide a zero-knowledge proof that demonstrates
+ * they know the exact encrypted balance amount.
+ *
+ * @par Cryptographic Operations:
+ * - **Equality Proof Verification**: Verifies that the issuer's revealed
+ *   amount matches the holder's encrypted balance using the issuer's
+ *   ElGamal private key.
+ *
+ * @see ConfidentialMPTSend, ConfidentialMPTConvert
+ */
+class ConfidentialMPTClawback : public Transactor
+{
+public:
+    static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
+
+    explicit ConfidentialMPTClawback(ApplyContext& ctx) : Transactor(ctx)
+    {
+    }
+
+    static NotTEC
+    preflight(PreflightContext const& ctx);
+
+    static XRPAmount
+    calculateBaseFee(ReadView const& view, STTx const& tx);
+
+    static TER
+    preclaim(PreclaimContext const& ctx);
+
+    TER
+    doApply() override;
+
+    void
+    visitInvariantEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after) override;
+
+    [[nodiscard]] bool
+    finalizeInvariants(
+        STTx const& tx,
+        TER result,
+        XRPAmount fee,
+        ReadView const& view,
+        beast::Journal const& j) override;
+};
+
+}  // namespace xrpl
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h
new file mode 100644
index 0000000000..2e2591844f
--- /dev/null
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include 
+
+namespace xrpl {
+
+/**
+ * @brief Converts public (plaintext) MPT balance to confidential (encrypted)
+ * balance.
+ *
+ * This transaction allows a token holder to convert their publicly visible
+ * MPToken balance into an encrypted confidential balance. Once converted,
+ * the balance can only be spent using ConfidentialMPTSend transactions and
+ * remains hidden from public view on the ledger.
+ *
+ * @par Cryptographic Operations:
+ * - **Schnorr Proof Verification**: When registering a new ElGamal public key,
+ *   verifies proof of knowledge of the corresponding private key.
+ * - **Revealed Amount Verification**: Verifies that the provided encrypted
+ *   amounts (for holder, issuer, and optionally auditor) all encrypt the
+ *   same plaintext amount using the provided blinding factor.
+ *
+ * @see ConfidentialMPTConvertBack, ConfidentialMPTSend
+ */
+class ConfidentialMPTConvert : public Transactor
+{
+public:
+    static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
+
+    explicit ConfidentialMPTConvert(ApplyContext& ctx) : Transactor(ctx)
+    {
+    }
+
+    static NotTEC
+    preflight(PreflightContext const& ctx);
+
+    static XRPAmount
+    calculateBaseFee(ReadView const& view, STTx const& tx);
+
+    static TER
+    preclaim(PreclaimContext const& ctx);
+
+    TER
+    doApply() override;
+
+    void
+    visitInvariantEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after) override;
+
+    [[nodiscard]] bool
+    finalizeInvariants(
+        STTx const& tx,
+        TER result,
+        XRPAmount fee,
+        ReadView const& view,
+        beast::Journal const& j) override;
+};
+
+}  // namespace xrpl
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h
new file mode 100644
index 0000000000..cb4b6295a0
--- /dev/null
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include 
+
+namespace xrpl {
+
+/**
+ * @brief Converts confidential (encrypted) MPT balance back to public
+ * (plaintext) balance.
+ *
+ * This transaction allows a token holder to convert their encrypted
+ * confidential balance back into a publicly visible MPToken balance. The
+ * holder must prove they have sufficient confidential balance without
+ * revealing the actual balance amount.
+ *
+ * @par Cryptographic Operations:
+ * - **Revealed Amount Verification**: Verifies that the provided encrypted
+ *   amounts correctly encrypt the conversion amount.
+ * - **Pedersen Linkage Proof**: Verifies that the provided balance commitment
+ *   correctly links to the holder's encrypted spending balance.
+ * - **Bulletproof Range Proof**: Verifies that the remaining balance (after
+ *   conversion) is non-negative, ensuring the holder has sufficient funds.
+ *
+ * @see ConfidentialMPTConvert, ConfidentialMPTSend
+ */
+class ConfidentialMPTConvertBack : public Transactor
+{
+public:
+    static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
+
+    explicit ConfidentialMPTConvertBack(ApplyContext& ctx) : Transactor(ctx)
+    {
+    }
+
+    static NotTEC
+    preflight(PreflightContext const& ctx);
+
+    static XRPAmount
+    calculateBaseFee(ReadView const& view, STTx const& tx);
+
+    static TER
+    preclaim(PreclaimContext const& ctx);
+
+    TER
+    doApply() override;
+
+    void
+    visitInvariantEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after) override;
+
+    [[nodiscard]] bool
+    finalizeInvariants(
+        STTx const& tx,
+        TER result,
+        XRPAmount fee,
+        ReadView const& view,
+        beast::Journal const& j) override;
+};
+
+}  // namespace xrpl
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h
new file mode 100644
index 0000000000..585c273e12
--- /dev/null
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include 
+
+namespace xrpl {
+
+/**
+ * @brief Merges the confidential inbox balance into the spending balance.
+ *
+ * In the confidential transfer system, incoming funds are deposited into an
+ * "inbox" balance that the recipient cannot immediately spend. This prevents
+ * front-running attacks where an attacker could invalidate a pending
+ * transaction by sending funds to the sender. This transaction merges the
+ * inbox into the spending balance, making those funds available for spending.
+ *
+ * @par Cryptographic Operations:
+ * - **Homomorphic Addition**: Adds the encrypted inbox balance to the
+ *   encrypted spending balance using ElGamal homomorphic properties.
+ * - **Zero Encryption**: Resets the inbox to an encryption of zero.
+ *
+ * @note This transaction requires no zero-knowledge proofs because it only
+ *       combines encrypted values that the holder already owns. The
+ *       homomorphic properties of ElGamal encryption ensure correctness.
+ *
+ * @see ConfidentialMPTSend, ConfidentialMPTConvert
+ */
+class ConfidentialMPTMergeInbox : public Transactor
+{
+public:
+    static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
+
+    explicit ConfidentialMPTMergeInbox(ApplyContext& ctx) : Transactor(ctx)
+    {
+    }
+
+    static NotTEC
+    preflight(PreflightContext const& ctx);
+
+    static XRPAmount
+    calculateBaseFee(ReadView const& view, STTx const& tx);
+
+    static TER
+    preclaim(PreclaimContext const& ctx);
+
+    TER
+    doApply() override;
+
+    void
+    visitInvariantEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after) override;
+
+    [[nodiscard]] bool
+    finalizeInvariants(
+        STTx const& tx,
+        TER result,
+        XRPAmount fee,
+        ReadView const& view,
+        beast::Journal const& j) override;
+};
+
+}  // namespace xrpl
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h
new file mode 100644
index 0000000000..72599ab987
--- /dev/null
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include 
+
+namespace xrpl {
+
+/**
+ * @brief Transfers confidential MPT tokens between holders privately.
+ *
+ * This transaction enables private token transfers where the transfer amount
+ * is hidden from public view. Both sender and recipient must have initialized
+ * confidential balances. The transaction provides encrypted amounts for all
+ * parties (sender, destination, issuer, and optionally auditor) along with
+ * zero-knowledge proofs that verify correctness without revealing the amount.
+ *
+ * @par Cryptographic Operations:
+ * - **Multi-Ciphertext Equality Proof**: Verifies that all encrypted amounts
+ *   (sender, destination, issuer, auditor) encrypt the same plaintext value.
+ * - **Amount Pedersen Linkage Proof**: Verifies that the amount commitment
+ *   correctly links to the sender's encrypted amount.
+ * - **Balance Pedersen Linkage Proof**: Verifies that the balance commitment
+ *   correctly links to the sender's encrypted spending balance.
+ * - **Bulletproof Range Proof**: Verifies remaining balance and
+ *   transfer amount are non-negative.
+ *
+ * @note Funds are deposited into the destination's inbox, not spending
+ *       balance. The recipient must call ConfidentialMPTMergeInbox to make
+ *       received funds spendable.
+ *
+ * @see ConfidentialMPTMergeInbox, ConfidentialMPTConvert,
+ * ConfidentialMPTConvertBack
+ */
+class ConfidentialMPTSend : public Transactor
+{
+public:
+    static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
+
+    explicit ConfidentialMPTSend(ApplyContext& ctx) : Transactor(ctx)
+    {
+    }
+
+    static bool
+    checkExtraFeatures(PreflightContext const& ctx);
+
+    static NotTEC
+    preflight(PreflightContext const& ctx);
+
+    static XRPAmount
+    calculateBaseFee(ReadView const& view, STTx const& tx);
+
+    static TER
+    preclaim(PreclaimContext const& ctx);
+
+    TER
+    doApply() override;
+
+    void
+    visitInvariantEntry(
+        bool isDelete,
+        std::shared_ptr const& before,
+        std::shared_ptr const& after) override;
+
+    [[nodiscard]] bool
+    finalizeInvariants(
+        STTx const& tx,
+        TER result,
+        XRPAmount fee,
+        ReadView const& view,
+        beast::Journal const& j) override;
+};
+
+}  // namespace xrpl
diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
index ca5876f88a..6fc2faf03e 100644
--- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
@@ -346,9 +346,9 @@ verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, b
 }
 
 TER
-verifyDepositPreauth(
+checkDepositPreauth(
     STTx const& tx,
-    ApplyView& view,
+    ReadView const& view,
     AccountID const& src,
     AccountID const& dst,
     SLE::const_ref sleDst,
@@ -360,9 +360,27 @@ verifyDepositPreauth(
     //  2. If src is deposit preauthorized by dst (either by account or by
     //  credentials).
 
-    bool const credentialsPresent = tx.isFieldPresent(sfCredentialIDs);
+    if (sleDst && ((sleDst->getFlags() & lsfDepositAuth) != 0u))
+    {
+        if (src != dst)
+        {
+            if (!view.exists(keylet::depositPreauth(dst, src)))
+            {
+                return !tx.isFieldPresent(sfCredentialIDs)
+                    ? tecNO_PERMISSION
+                    : credentials::authorizedDepositPreauth(
+                          view, tx.getFieldV256(sfCredentialIDs), dst);
+            }
+        }
+    }
 
-    if (credentialsPresent)
+    return tesSUCCESS;
+}
+
+TER
+cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j)
+{
+    if (tx.isFieldPresent(sfCredentialIDs))
     {
         auto const foundExpired =
             credentials::removeExpired(view, tx.getFieldV256(sfCredentialIDs), j);
@@ -372,20 +390,22 @@ verifyDepositPreauth(
             return tecEXPIRED;
     }
 
-    if (sleDst && sleDst->isFlag(lsfDepositAuth))
-    {
-        if (src != dst)
-        {
-            if (!view.exists(keylet::depositPreauth(dst, src)))
-            {
-                return !credentialsPresent ? tecNO_PERMISSION
-                                           : credentials::authorizedDepositPreauth(
-                                                 view, tx.getFieldV256(sfCredentialIDs), dst);
-            }
-        }
-    }
-
     return tesSUCCESS;
 }
 
+TER
+verifyDepositPreauth(
+    STTx const& tx,
+    ApplyView& view,
+    AccountID const& src,
+    AccountID const& dst,
+    SLE::const_ref sleDst,
+    beast::Journal j)
+{
+    if (auto const err = cleanupExpiredCredentials(tx, view, j); !isTesSuccess(err))
+        return err;
+
+    return checkDepositPreauth(tx, view, src, dst, sleDst, j);
+}
+
 }  // namespace xrpl
diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
index 2781902f5b..3c4e55a16e 100644
--- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
@@ -290,6 +290,15 @@ removeEmptyHolding(
         (view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0))
         return tecHAS_OBLIGATIONS;
 
+    // Don't delete if the token still has confidential balances
+    if (mptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
+        mptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
+        mptoken->isFieldPresent(sfIssuerEncryptedBalance) ||
+        mptoken->isFieldPresent(sfAuditorEncryptedBalance))
+    {
+        return tecHAS_OBLIGATIONS;
+    }
+
     return authorizeMPToken(
         view,
         {},  // priorBalance
diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
index b9adfbd3e6..2ecde25754 100644
--- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
@@ -426,7 +426,8 @@ accountHolds(
         // Only if auth check is needed, as it needs to do an additional read
         // operation. Note featureSingleAssetVault will affect error codes.
         if (zeroIfUnauthorized == AuthHandling::ZeroIfUnauthorized &&
-            view.rules().enabled(featureSingleAssetVault))
+            (view.rules().enabled(featureSingleAssetVault) ||
+             view.rules().enabled(featureConfidentialTransfer)))
         {
             if (auto const err = requireAuth(view, mptIssue, account, AuthType::StrongAuth);
                 !isTesSuccess(err))
diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp
new file mode 100644
index 0000000000..0baff1e33a
--- /dev/null
+++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp
@@ -0,0 +1,487 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+namespace {
+
+account_id
+toAccountId(AccountID const& account)
+{
+    account_id res;
+    std::memcpy(res.bytes, account.data(), kMPT_ACCOUNT_ID_SIZE);
+    return res;
+}
+
+mpt_issuance_id
+toIssuanceId(uint192 const& issuance)
+{
+    mpt_issuance_id res;
+    std::memcpy(res.bytes, issuance.data(), kMPT_ISSUANCE_ID_SIZE);
+    return res;
+}
+
+/**
+ * @brief Pack a ConfidentialRecipient (public key + ElGamal ciphertext) into the
+ * secp256k1-mpt participant struct. Callers MUST have already validated that
+ * r.publicKey.size() == kEcPubKeyLength and
+ * r.encryptedAmount.size() == kEcGamalEncryptedTotalLength;
+ */
+mpt_confidential_participant
+toParticipant(ConfidentialRecipient const& r)
+{
+    mpt_confidential_participant p{};
+    std::memcpy(p.pubkey, r.publicKey.data(), kEcPubKeyLength);
+    std::memcpy(p.ciphertext, r.encryptedAmount.data(), kEcGamalEncryptedTotalLength);
+    return p;
+}
+
+}  // namespace
+
+uint256
+getSendContextHash(
+    AccountID const& account,
+    uint192 const& issuanceID,
+    std::uint32_t sequence,
+    AccountID const& destination,
+    std::uint32_t version)
+{
+    uint256 result;
+    mpt_get_send_context_hash(
+        toAccountId(account),
+        toIssuanceId(issuanceID),
+        sequence,
+        toAccountId(destination),
+        version,
+        result.data());
+    return result;
+}
+
+uint256
+getClawbackContextHash(
+    AccountID const& account,
+    uint192 const& issuanceID,
+    std::uint32_t sequence,
+    AccountID const& holder)
+{
+    uint256 result;
+    mpt_get_clawback_context_hash(
+        toAccountId(account),
+        toIssuanceId(issuanceID),
+        sequence,
+        toAccountId(holder),
+        result.data());
+    return result;
+}
+
+uint256
+getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence)
+{
+    uint256 result;
+    mpt_get_convert_context_hash(
+        toAccountId(account), toIssuanceId(issuanceID), sequence, result.data());
+    return result;
+}
+
+uint256
+getConvertBackContextHash(
+    AccountID const& account,
+    uint192 const& issuanceID,
+    std::uint32_t sequence,
+    std::uint32_t version)
+{
+    uint256 result;
+    mpt_get_convert_back_context_hash(
+        toAccountId(account), toIssuanceId(issuanceID), sequence, version, result.data());
+    return result;
+}
+
+std::optional
+makeEcPair(Slice const& buffer)
+{
+    if (buffer.length() != 2 * kEcCiphertextComponentLength)
+        return std::nullopt;  // LCOV_EXCL_LINE
+
+    auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
+        return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length());
+    };
+
+    Slice const s1{buffer.data(), kEcCiphertextComponentLength};
+    Slice const s2{buffer.data() + kEcCiphertextComponentLength, kEcCiphertextComponentLength};
+
+    EcPair pair{};
+    if (parsePubKey(s1, pair.c1) != 1 || parsePubKey(s2, pair.c2) != 1)
+        return std::nullopt;
+
+    return pair;
+}
+
+std::optional
+serializeEcPair(EcPair const& pair)
+{
+    auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) {
+        size_t outLen = kEcCiphertextComponentLength;  // 33 bytes
+        auto const ret = secp256k1_ec_pubkey_serialize(
+            secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED);
+        return ret == 1 && outLen == kEcCiphertextComponentLength;
+    };
+
+    Buffer buffer(kEcGamalEncryptedTotalLength);
+    auto const ptr = buffer.data();
+    bool const res1 = serializePubKey(pair.c1, ptr);
+    bool const res2 = serializePubKey(pair.c2, ptr + kEcCiphertextComponentLength);
+
+    if (!res1 || !res2)
+        return std::nullopt;
+
+    return buffer;
+}
+
+bool
+isValidCiphertext(Slice const& buffer)
+{
+    return makeEcPair(buffer).has_value();
+}
+
+bool
+isValidCompressedECPoint(Slice const& buffer)
+{
+    if (buffer.size() != kCompressedEcPointLength)
+        return false;
+
+    // Compressed EC points must start with 0x02 or 0x03
+    if (buffer[0] != kEcCompressedPrefixEvenY && buffer[0] != kEcCompressedPrefixOddY)
+        return false;
+
+    secp256k1_pubkey point;
+    return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1;
+}
+
+std::optional
+homomorphicAdd(Slice const& a, Slice const& b)
+{
+    if (a.length() != kEcGamalEncryptedTotalLength || b.length() != kEcGamalEncryptedTotalLength)
+        return std::nullopt;
+
+    auto const pairA = makeEcPair(a);
+    auto const pairB = makeEcPair(b);
+
+    if (!pairA || !pairB)
+        return std::nullopt;
+
+    EcPair sum{};
+    if (auto res = secp256k1_elgamal_add(
+            secp256k1Context(), &sum.c1, &sum.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2);
+        res != 1)
+    {
+        return std::nullopt;
+    }
+
+    return serializeEcPair(sum);
+}
+
+std::optional
+homomorphicSubtract(Slice const& a, Slice const& b)
+{
+    if (a.length() != kEcGamalEncryptedTotalLength || b.length() != kEcGamalEncryptedTotalLength)
+        return std::nullopt;
+
+    auto const pairA = makeEcPair(a);
+    auto const pairB = makeEcPair(b);
+
+    if (!pairA || !pairB)
+        return std::nullopt;
+
+    EcPair diff{};
+    if (auto const res = secp256k1_elgamal_subtract(
+            secp256k1Context(), &diff.c1, &diff.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2);
+        res != 1)
+    {
+        return std::nullopt;
+    }
+
+    return serializeEcPair(diff);
+}
+
+std::optional
+rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness)
+{
+    auto zero = encryptAmount(0, pubKeySlice, randomness);
+    if (!zero)
+        return std::nullopt;
+
+    return homomorphicAdd(ciphertext, *zero);
+}
+
+Buffer
+generateBlindingFactor()
+{
+    unsigned char blindingFactor[kEcBlindingFactorLength];
+
+    // todo: might need to be updated using another RNG
+    if (RAND_bytes(blindingFactor, kEcBlindingFactorLength) != 1)
+        Throw("Failed to generate random number");
+
+    return Buffer(blindingFactor, kEcBlindingFactorLength);
+}
+
+std::optional
+encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor)
+{
+    if (blindingFactor.size() != kEcBlindingFactorLength || pubKeySlice.size() != kEcPubKeyLength)
+        return std::nullopt;
+
+    Buffer out(kEcGamalEncryptedTotalLength);
+    if (mpt_encrypt_amount(amt, pubKeySlice.data(), blindingFactor.data(), out.data()) != 0)
+        return std::nullopt;
+
+    return out;
+}
+
+std::optional
+encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
+{
+    if (pubKeySlice.size() != kEcPubKeyLength)
+        return std::nullopt;  // LCOV_EXCL_LINE
+
+    EcPair pair{};
+    secp256k1_pubkey pubKey;
+    if (auto res = secp256k1_ec_pubkey_parse(
+            secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength);
+        res != 1)
+    {
+        return std::nullopt;  // LCOV_EXCL_LINE
+    }
+
+    if (auto res = generate_canonical_encrypted_zero(
+            secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data());
+        res != 1)
+    {
+        return std::nullopt;  // LCOV_EXCL_LINE
+    }
+
+    return serializeEcPair(pair);
+}
+
+TER
+verifyRevealedAmount(
+    uint64_t const amount,
+    Slice const& blindingFactor,
+    ConfidentialRecipient const& holder,
+    ConfidentialRecipient const& issuer,
+    std::optional const& auditor)
+{
+    if (blindingFactor.size() != kEcBlindingFactorLength ||
+        holder.publicKey.size() != kEcPubKeyLength ||
+        holder.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
+        issuer.publicKey.size() != kEcPubKeyLength ||
+        issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength)
+    {
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    auto const holderP = toParticipant(holder);
+    auto const issuerP = toParticipant(issuer);
+    mpt_confidential_participant auditorP{};
+    mpt_confidential_participant const* auditorPtr = nullptr;
+    if (auditor)
+    {
+        if (auditor->publicKey.size() != kEcPubKeyLength ||
+            auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
+        {
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+        }
+        auditorP = toParticipant(*auditor);
+        auditorPtr = &auditorP;
+    }
+
+    if (mpt_verify_revealed_amount(amount, blindingFactor.data(), &holderP, &issuerP, auditorPtr) !=
+        0)
+    {
+        return tecBAD_PROOF;
+    }
+
+    return tesSUCCESS;
+}
+
+NotTEC
+checkEncryptedAmountFormat(STObject const& object)
+{
+    // Current usage of this function is only for ConfidentialMPTConvert and
+    // ConfidentialMPTConvertBack transactions, which already enforce that these fields
+    // are present.
+    if (!object.isFieldPresent(sfHolderEncryptedAmount) ||
+        !object.isFieldPresent(sfIssuerEncryptedAmount))
+    {
+        return temMALFORMED;  // LCOV_EXCL_LINE
+    }
+
+    if (object[sfHolderEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
+        object[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
+    {
+        return temBAD_CIPHERTEXT;
+    }
+
+    bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount);
+    if (hasAuditor && object[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
+        return temBAD_CIPHERTEXT;
+
+    if (!isValidCiphertext(object[sfHolderEncryptedAmount]) ||
+        !isValidCiphertext(object[sfIssuerEncryptedAmount]))
+    {
+        return temBAD_CIPHERTEXT;
+    }
+
+    if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount]))
+        return temBAD_CIPHERTEXT;
+
+    return tesSUCCESS;
+}
+
+TER
+verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
+{
+    if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0)
+        return tecBAD_PROOF;
+
+    return tesSUCCESS;
+}
+
+TER
+verifyClawbackProof(
+    uint64_t const amount,
+    Slice const& proof,
+    Slice const& pubKeySlice,
+    Slice const& ciphertext,
+    uint256 const& contextHash)
+{
+    if (ciphertext.size() != kEcGamalEncryptedTotalLength ||
+        pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength)
+    {
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    if (mpt_verify_clawback_proof(
+            proof.data(), amount, pubKeySlice.data(), ciphertext.data(), contextHash.data()) != 0)
+    {
+        return tecBAD_PROOF;
+    }
+
+    return tesSUCCESS;
+}
+
+TER
+verifySendProof(
+    Slice const& proof,
+    ConfidentialRecipient const& sender,
+    ConfidentialRecipient const& destination,
+    ConfidentialRecipient const& issuer,
+    std::optional const& auditor,
+    Slice const& spendingBalance,
+    Slice const& amountCommitment,
+    Slice const& balanceCommitment,
+    uint256 const& contextHash)
+{
+    auto const recipientCount = getConfidentialRecipientCount(auditor.has_value());
+    if (proof.size() != kEcSendProofLength || sender.publicKey.size() != kEcPubKeyLength ||
+        sender.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
+        destination.publicKey.size() != kEcPubKeyLength ||
+        destination.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
+        issuer.publicKey.size() != kEcPubKeyLength ||
+        issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
+        spendingBalance.size() != kEcGamalEncryptedTotalLength ||
+        amountCommitment.size() != kEcPedersenCommitmentLength ||
+        balanceCommitment.size() != kEcPedersenCommitmentLength)
+    {
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    std::vector participants;
+    participants.reserve(recipientCount);
+    participants.push_back(toParticipant(sender));
+    participants.push_back(toParticipant(destination));
+    participants.push_back(toParticipant(issuer));
+    if (auditor)
+    {
+        if (auditor->publicKey.size() != kEcPubKeyLength ||
+            auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
+        {
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+        }
+        participants.push_back(toParticipant(*auditor));
+    }
+    if (participants.size() != recipientCount)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    if (mpt_verify_send_proof(
+            proof.data(),
+            participants.data(),
+            recipientCount,
+            spendingBalance.data(),
+            amountCommitment.data(),
+            balanceCommitment.data(),
+            contextHash.data()) != 0)
+    {
+        return tecBAD_PROOF;
+    }
+
+    return tesSUCCESS;
+}
+
+TER
+verifyConvertBackProof(
+    Slice const& proof,
+    Slice const& pubKeySlice,
+    Slice const& spendingBalance,
+    Slice const& balanceCommitment,
+    uint64_t amount,
+    uint256 const& contextHash)
+{
+    if (proof.size() != kEcConvertBackProofLength || pubKeySlice.size() != kEcPubKeyLength ||
+        spendingBalance.size() != kEcGamalEncryptedTotalLength ||
+        balanceCommitment.size() != kEcPedersenCommitmentLength)
+    {
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    if (mpt_verify_convert_back_proof(
+            proof.data(),
+            pubKeySlice.data(),
+            spendingBalance.data(),
+            balanceCommitment.data(),
+            amount,
+            contextHash.data()) != 0)
+    {
+        return tecBAD_PROOF;
+    }
+
+    return tesSUCCESS;
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/protocol/PublicKey.cpp b/src/libxrpl/protocol/PublicKey.cpp
index 7472f059e9..c6e6c1d324 100644
--- a/src/libxrpl/protocol/PublicKey.cpp
+++ b/src/libxrpl/protocol/PublicKey.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -211,7 +212,7 @@ publicKeyType(Slice const& slice)
         if (slice[0] == 0xED)
             return KeyType::Ed25519;
 
-        if (slice[0] == 0x02 || slice[0] == 0x03)
+        if (slice[0] == kEcCompressedPrefixEvenY || slice[0] == kEcCompressedPrefixOddY)
             return KeyType::Secp256k1;
     }
 
diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp
index 6b8dfc6811..e5c1d17b1a 100644
--- a/src/libxrpl/protocol/TER.cpp
+++ b/src/libxrpl/protocol/TER.cpp
@@ -106,6 +106,7 @@ transResults()
         MAKE_ERROR(tecLIMIT_EXCEEDED,                "Limit exceeded."),
         MAKE_ERROR(tecPSEUDO_ACCOUNT,                "This operation is not allowed against a pseudo-account."),
         MAKE_ERROR(tecPRECISION_LOSS,                "The amounts used by the transaction cannot interact."),
+        MAKE_ERROR(tecBAD_PROOF,                     "Proof cannot be verified"),
 
         MAKE_ERROR(tefALREADY,                     "The exact transaction was already in this ledger."),
         MAKE_ERROR(tefBAD_ADD_AUTH,                "Not authorized to add account."),
@@ -199,6 +200,7 @@ transResults()
         MAKE_ERROR(temARRAY_TOO_LARGE,           "Malformed: Array is too large."),
         MAKE_ERROR(temBAD_TRANSFER_FEE,          "Malformed: Transfer fee is outside valid range."),
         MAKE_ERROR(temINVALID_INNER_BATCH,       "Malformed: Invalid inner batch transaction."),
+        MAKE_ERROR(temBAD_CIPHERTEXT,            "Malformed: Invalid ciphertext."),
 
         MAKE_ERROR(terRETRY,                  "Retry transaction."),
         MAKE_ERROR(terFUNDS_SPENT,            "DEPRECATED."),
diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp
index 3a10b7124f..9ab8779f03 100644
--- a/src/libxrpl/tx/Transactor.cpp
+++ b/src/libxrpl/tx/Transactor.cpp
@@ -356,6 +356,15 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
     return baseFee + (signerCount * baseFee);
 }
 
+XRPAmount
+Transactor::calculateBaseFee(
+    ReadView const& view,
+    STTx const& tx,
+    std::uint32_t extraBaseFeeMultiplier)
+{
+    return calculateBaseFee(view, tx) + view.fees().base * extraBaseFeeMultiplier;
+}
+
 // Returns the fee in fee units, not scaled for load.
 XRPAmount
 Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx)
diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp
index 278c7a7858..26bee4effb 100644
--- a/src/libxrpl/tx/invariants/MPTInvariant.cpp
+++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp
@@ -1,7 +1,9 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -23,11 +25,46 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
 #include 
+
 namespace xrpl {
 
+namespace {
+constexpr auto kConfidentialMptTxTypes = std::to_array({
+    ttCONFIDENTIAL_MPT_SEND,
+    ttCONFIDENTIAL_MPT_CONVERT,
+    ttCONFIDENTIAL_MPT_CONVERT_BACK,
+    ttCONFIDENTIAL_MPT_MERGE_INBOX,
+    ttCONFIDENTIAL_MPT_CLAWBACK,
+});
+
+// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant
+// tests can inject INT64_MAX + 1, which would result in undefined behavior
+// under UBSan if converted directly.
+std::int64_t
+toSignedMPTAmount(std::uint64_t amount)
+{
+    return static_cast(std::min(amount, kMaxMpTokenAmount));
+}
+
+std::int64_t
+addMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
+{
+    return delta + toSignedMPTAmount(amount);
+}
+
+std::int64_t
+subtractMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
+{
+    return delta - toSignedMPTAmount(amount);
+}
+
+}  // namespace
+
 void
 ValidMPTIssuance::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
 {
@@ -438,6 +475,16 @@ ValidMPTPayment::finalize(
 {
     if (isTesSuccess(result))
     {
+        // Confidential transactions are validated by ValidConfidentialMPToken.
+        // They modify encrypted fields and sfConfidentialOutstandingAmount
+        // rather than sfMPTAmount/sfOutstandingAmount in the standard way,
+        // so ValidMPTPayment's accounting does not apply to them.
+        if (std::ranges::find(kConfidentialMptTxTypes, tx.getTxnType()) !=
+            kConfidentialMptTxTypes.end())
+        {
+            return true;
+        }
+
         bool const invariantPasses = !view.rules().enabled(featureMPTokensV2);
         if (overflow_)
         {
@@ -468,6 +515,261 @@ ValidMPTPayment::finalize(
     return true;
 }
 
+void
+ValidConfidentialMPToken::visitEntry(
+    bool isDelete,
+    std::shared_ptr const& before,
+    std::shared_ptr const& after)
+{
+    // Helper to get MPToken Issuance ID safely
+    auto const getMptID = [](std::shared_ptr const& sle) -> uint192 {
+        if (!sle)
+            return beast::kZero;
+        if (sle->getType() == ltMPTOKEN)
+            return sle->getFieldH192(sfMPTokenIssuanceID);
+        if (sle->getType() == ltMPTOKEN_ISSUANCE)
+            return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer));
+        return beast::kZero;
+    };
+
+    if (before && before->getType() == ltMPTOKEN)
+    {
+        uint192 const id = getMptID(before);
+        auto& change = changes_[id];
+        change.mptAmountDelta =
+            subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount));
+
+        // Cannot delete MPToken with non-zero confidential state or non-zero public amount
+        if (isDelete)
+        {
+            bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
+            bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
+                before->isFieldPresent(sfConfidentialBalanceInbox) ||
+                before->isFieldPresent(sfIssuerEncryptedBalance) ||
+                before->isFieldPresent(sfAuditorEncryptedBalance);
+
+            if (hasPublicBalance || hasEncryptedFields)
+                changes_[id].deletedWithEncrypted = true;
+        }
+    }
+
+    if (after && after->getType() == ltMPTOKEN)
+    {
+        uint192 const id = getMptID(after);
+        auto& change = changes_[id];
+        change.mptAmountDelta =
+            addMPTAmountDelta(change.mptAmountDelta, after->getFieldU64(sfMPTAmount));
+
+        // Encrypted field existence consistency
+        bool const hasIssuerBalance = after->isFieldPresent(sfIssuerEncryptedBalance);
+        bool const hasHolderInbox = after->isFieldPresent(sfConfidentialBalanceInbox);
+        bool const hasHolderSpending = after->isFieldPresent(sfConfidentialBalanceSpending);
+        bool const hasAuditorBalance = after->isFieldPresent(sfAuditorEncryptedBalance);
+
+        // The core encrypted balances must all exist or not exist at the same time. The auditor
+        // balance is optional, but cannot exist without the core fields.
+        if (hasHolderInbox != hasHolderSpending || hasHolderInbox != hasIssuerBalance ||
+            (hasAuditorBalance && !hasIssuerBalance))
+            changes_[id].badConsistency = true;
+
+        auto const confidentialBalanceFieldChanged = [&before, &after](auto const& field) {
+            auto const afterValue = (*after)[~field];
+            if (!afterValue)
+                return false;
+
+            if (!before || before->getType() != ltMPTOKEN)
+                return true;  // LCOV_EXCL_LINE
+
+            return (*before)[~field] != afterValue;
+        };
+
+        if (confidentialBalanceFieldChanged(sfConfidentialBalanceInbox) ||
+            confidentialBalanceFieldChanged(sfConfidentialBalanceSpending) ||
+            confidentialBalanceFieldChanged(sfIssuerEncryptedBalance) ||
+            confidentialBalanceFieldChanged(sfAuditorEncryptedBalance))
+        {
+            changes_[id].changesConfidentialFields = true;
+        }
+    }
+
+    if (before && before->getType() == ltMPTOKEN_ISSUANCE)
+    {
+        uint192 const id = getMptID(before);
+        auto& change = changes_[id];
+        if (before->isFieldPresent(sfConfidentialOutstandingAmount))
+        {
+            change.coaDelta = subtractMPTAmountDelta(
+                change.coaDelta, before->getFieldU64(sfConfidentialOutstandingAmount));
+        }
+        change.outstandingDelta = subtractMPTAmountDelta(
+            change.outstandingDelta, before->getFieldU64(sfOutstandingAmount));
+    }
+
+    if (after && after->getType() == ltMPTOKEN_ISSUANCE)
+    {
+        uint192 const id = getMptID(after);
+        auto& change = changes_[id];
+
+        bool const hasCOA = after->isFieldPresent(sfConfidentialOutstandingAmount);
+        std::uint64_t const coa = (*after)[~sfConfidentialOutstandingAmount].value_or(0);
+        std::uint64_t const oa = after->getFieldU64(sfOutstandingAmount);
+
+        if (hasCOA)
+            change.coaDelta = addMPTAmountDelta(change.coaDelta, coa);
+
+        change.outstandingDelta = addMPTAmountDelta(change.outstandingDelta, oa);
+        change.issuance = after;
+
+        // COA <= OutstandingAmount
+        if (coa > oa)
+            change.badCOA = true;
+    }
+
+    if (before && after && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN)
+    {
+        uint192 const id = getMptID(after);
+
+        // sfConfidentialBalanceVersion must change when spending changes
+        auto const spendingBefore = (*before)[~sfConfidentialBalanceSpending];
+        auto const spendingAfter = (*after)[~sfConfidentialBalanceSpending];
+        auto const versionBefore = (*before)[~sfConfidentialBalanceVersion];
+        auto const versionAfter = (*after)[~sfConfidentialBalanceVersion];
+
+        if (spendingBefore.has_value() && spendingBefore != spendingAfter)
+        {
+            if (versionBefore == versionAfter)
+                changes_[id].badVersion = true;
+        }
+    }
+}
+
+bool
+ValidConfidentialMPToken::finalize(
+    STTx const& tx,
+    TER const result,
+    XRPAmount const,
+    ReadView const& view,
+    beast::Journal const& j)
+{
+    if (result != tesSUCCESS)
+        return true;
+
+    for (auto const& [id, checks] : changes_)
+    {
+        // Find the MPTokenIssuance
+        auto const issuance = [&]() -> std::shared_ptr {
+            if (checks.issuance)
+                return checks.issuance;
+            return view.read(keylet::mptokenIssuance(id));
+        }();
+
+        // Skip all invariance checks if issuance doesn't exist because that means the MPT has been
+        // deleted
+        if (!issuance)
+            continue;
+
+        // Cannot delete MPToken with non-zero confidential state
+        if (checks.deletedWithEncrypted)
+        {
+            if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
+            {
+                JLOG(j.fatal())
+                    << "Invariant failed: MPToken deleted with encrypted fields while COA > 0";
+                return false;
+            }
+        }
+
+        // Encrypted field existence consistency
+        if (checks.badConsistency)
+        {
+            JLOG(j.fatal()) << "Invariant failed: MPToken encrypted field "
+                               "existence inconsistency";
+            return false;
+        }
+
+        // COA <= OutstandingAmount
+        if (checks.badCOA)
+        {
+            JLOG(j.fatal()) << "Invariant failed: Confidential outstanding amount "
+                               "exceeds total outstanding amount";
+            return false;
+        }
+
+        // Confidential balance fields may remain on a holder MPToken after all
+        // confidential balances have returned to zero. Only creating or
+        // changing those fields requires the issuance privacy flag.
+        if (checks.changesConfidentialFields)
+        {
+            if (!issuance->isFlag(lsfMPTCanHoldConfidentialBalance))
+            {
+                JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted "
+                                   "fields but Issuance does not have "
+                                   "lsfMPTCanHoldConfidentialBalance set";
+                return false;
+            }
+        }
+
+        // We only enforce this when Confidential Outstanding Amount changes (Convert, ConvertBack,
+        // ConfidentialClawback). This avoids falsely failing on Escrow or AMM operations that lock
+        // public tokens outside of ltMPTOKEN. Convert / ConvertBack:
+        // - COA and MPTAmount must have opposite deltas, which cancel each other out to zero.
+        // - OA remains unchanged.
+        // - Therefore, the net delta on both sides of the equation is zero.
+        //
+        // Clawback:
+        // - MPTAmount remains unchanged.
+        // - COA and OA must have identical deltas (mirrored on each side).
+        // - The equation remains balanced as both sides have equal offsets.
+        if (checks.coaDelta != 0)
+        {
+            if (checks.mptAmountDelta + checks.coaDelta != checks.outstandingDelta)
+            {
+                JLOG(j.fatal()) << "Invariant failed: Token conservation "
+                                   "violation for MPT "
+                                << to_string(id);
+                return false;
+            }
+        }
+        else if (
+            std::ranges::find(kConfidentialMptTxTypes, tx.getTxnType()) !=
+            kConfidentialMptTxTypes.end())
+        {
+            // Confidential Txns should not modify public MPTAmount balance
+            // if Confidential Amount Delta is 0
+            if (checks.mptAmountDelta != 0)
+            {
+                JLOG(j.fatal()) << "Invariant failed: MPTAmount changed by confidential "
+                                   "transaction that should not modify this field."
+                                << to_string(id);
+                return false;
+            }
+
+            // Among confidential MPT transactions, only ConfidentialMPTSend and
+            // ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT
+            // transaction reaches here, it must be one of these two types, neither of which will
+            // modify sfOutstandingAmount
+            if (checks.outstandingDelta != 0)
+            {
+                JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed "
+                                   "by confidential transaction that should not "
+                                   "modify it for MPT "
+                                << to_string(id);
+                return false;
+            }
+        }
+
+        if (checks.badVersion)
+        {
+            JLOG(j.fatal())
+                << "Invariant failed: MPToken sfConfidentialBalanceVersion not updated when "
+                   "sfConfidentialBalanceSpending changed";
+            return false;
+        }
+    }
+
+    return true;
+}
+
 void
 ValidMPTTransfer::visitEntry(
     bool isDelete,
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp
new file mode 100644
index 0000000000..e0d1d28a8f
--- /dev/null
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp
@@ -0,0 +1,210 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+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
+    if (account != MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
+        return temMALFORMED;
+
+    // Cannot clawback from self
+    if (account == ctx.tx[sfHolder])
+        return temMALFORMED;
+
+    // Check invalid claw amount
+    auto const clawAmount = ctx.tx[sfMPTAmount];
+    if (clawAmount == 0 || clawAmount > kMaxMpTokenAmount)
+        return temBAD_AMOUNT;
+
+    // Verify proof length
+    if (ctx.tx[sfZKProof].length() != kEcClawbackProofLength)
+        return temMALFORMED;
+
+    return tesSUCCESS;
+}
+
+XRPAmount
+ConfidentialMPTClawback::calculateBaseFee(ReadView const& view, STTx const& tx)
+{
+    return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
+}
+
+TER
+ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
+{
+    // Check if sender account exists
+    auto const account = ctx.tx[sfAccount];
+    if (!ctx.view.exists(keylet::account(account)))
+        return terNO_ACCOUNT;
+
+    // Check if holder account exists
+    auto const holder = ctx.tx[sfHolder];
+    if (!ctx.view.exists(keylet::account(holder)))
+        return tecNO_TARGET;
+
+    // Check if MPT issuance exists
+    auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
+    if (!sleIssuance)
+        return tecOBJECT_NOT_FOUND;
+
+    // Sanity check: account must be the same as issuer
+    if (sleIssuance->getAccountID(sfIssuer) != account)
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+
+    // Check if issuance has issuer ElGamal public key
+    if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
+        return tecNO_PERMISSION;
+
+    // Check if clawback is allowed
+    if (!sleIssuance->isFlag(lsfMPTCanClawback))
+        return tecNO_PERMISSION;
+
+    // Check if issuance allows confidential transfer
+    if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
+        return tecNO_PERMISSION;
+
+    // Check holder's MPToken
+    auto const sleHolderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, holder));
+    if (!sleHolderMPToken)
+        return tecOBJECT_NOT_FOUND;
+
+    // Check if holder has confidential balances to claw back
+    if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
+        return tecNO_PERMISSION;
+
+    // Check if Holder has ElGamal public Key
+    if (!sleHolderMPToken->isFieldPresent(sfHolderEncryptionKey))
+        return tecNO_PERMISSION;
+
+    // Sanity check: claw amount can not exceed confidential outstanding amount
+    // or total outstanding amount (prevents underflow in doApply)
+    auto const amount = ctx.tx[sfMPTAmount];
+    if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) ||
+        amount > (*sleIssuance)[sfOutstandingAmount])
+        return tecINSUFFICIENT_FUNDS;
+
+    auto const contextHash =
+        getClawbackContextHash(account, mptIssuanceID, ctx.tx.getSeqProxy().value(), holder);
+
+    // Verify the revealed confidential amount by the issuer matches the exact
+    // confidential balance of the holder.
+    return verifyClawbackProof(
+        amount,
+        ctx.tx[sfZKProof],
+        (*sleIssuance)[sfIssuerEncryptionKey],
+        (*sleHolderMPToken)[sfIssuerEncryptedBalance],
+        contextHash);
+}
+
+TER
+ConfidentialMPTClawback::doApply()
+{
+    auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
+    auto const holder = ctx_.tx[sfHolder];
+
+    auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
+    auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
+
+    if (!sleIssuance || !sleHolderMPToken)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const clawAmount = ctx_.tx[sfMPTAmount];
+
+    auto const holderPubKey = (*sleHolderMPToken)[sfHolderEncryptionKey];
+    auto const issuerPubKey = (*sleIssuance)[sfIssuerEncryptionKey];
+
+    // After clawback, the balance should be encrypted zero.
+    auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
+    if (!encZeroForHolder)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
+    if (!encZeroForIssuer)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    // Set holder's confidential balances to encrypted zero
+    (*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder;
+    (*sleHolderMPToken)[sfConfidentialBalanceSpending] = *encZeroForHolder;
+    (*sleHolderMPToken)[sfIssuerEncryptedBalance] = std::move(*encZeroForIssuer);
+    incrementConfidentialVersion(*sleHolderMPToken);
+
+    if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
+    {
+        // Sanity check: the issuance must have an auditor public key if
+        // auditing is enabled.
+        if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey))
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey];
+
+        auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
+
+        if (!encZeroForAuditor)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        (*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor);
+    }
+
+    // Decrease Global Confidential Outstanding Amount
+    auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
+    if (clawAmount > oldCOA)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    (*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount;
+
+    // Decrease Global Total Outstanding Amount
+    auto const oldOA = (*sleIssuance)[sfOutstandingAmount];
+    if (clawAmount > oldOA)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    (*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount;
+
+    view().update(sleHolderMPToken);
+    view().update(sleIssuance);
+
+    return tesSUCCESS;
+}
+
+void
+ConfidentialMPTClawback::visitInvariantEntry(
+    bool,
+    std::shared_ptr const&,
+    std::shared_ptr const&)
+{
+}
+
+bool
+ConfidentialMPTClawback::finalizeInvariants(
+    STTx const&,
+    TER,
+    XRPAmount,
+    ReadView const&,
+    beast::Journal const&)
+{
+    return true;
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp
new file mode 100644
index 0000000000..44e2596325
--- /dev/null
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp
@@ -0,0 +1,350 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+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;
+
+    if (ctx.tx[sfMPTAmount] > kMaxMpTokenAmount)
+        return temBAD_AMOUNT;
+
+    if (ctx.tx.isFieldPresent(sfHolderEncryptionKey))
+    {
+        if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey]))
+            return temMALFORMED;
+
+        // proof of knowledge of the secret key corresponding to the provided
+        // public key is needed when holder ec public key is being set.
+        if (!ctx.tx.isFieldPresent(sfZKProof))
+            return temMALFORMED;
+
+        // verify schnorr proof length when registering holder ec public key
+        if (ctx.tx[sfZKProof].size() != kEcSchnorrProofLength)
+            return temMALFORMED;
+    }
+    else
+    {
+        // Either both sfHolderEncryptionKey and sfZKProof should be present, or both should be
+        // absent.
+        if (ctx.tx.isFieldPresent(sfZKProof))
+            return temMALFORMED;
+    }
+
+    // check encrypted amount format after the above basic checks
+    // this check is more expensive so put it at the end
+    if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
+        return res;
+
+    return tesSUCCESS;
+}
+
+XRPAmount
+ConfidentialMPTConvert::calculateBaseFee(ReadView const& view, STTx const& tx)
+{
+    return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
+}
+
+TER
+ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
+{
+    auto const account = ctx.tx[sfAccount];
+    auto const issuanceID = ctx.tx[sfMPTokenIssuanceID];
+    auto const amount = ctx.tx[sfMPTAmount];
+
+    // ensure that issuance exists
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(issuanceID));
+    if (!sleIssuance)
+        return tecOBJECT_NOT_FOUND;
+
+    if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
+        !sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // already checked in preflight, but should also check that issuer on the
+    // issuance isn't the account either
+    if (sleIssuance->getAccountID(sfIssuer) == account)
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+
+    bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
+    bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
+
+    // tx must include auditor ciphertext if the issuance has enabled
+    // auditing, and must not include it if auditing is not enabled
+    if (requiresAuditor != hasAuditor)
+        return tecNO_PERMISSION;
+
+    auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account));
+    if (!sleMptoken)
+        return tecOBJECT_NOT_FOUND;
+
+    auto const mptIssue = MPTIssue{issuanceID};
+
+    // Explicit freeze and auth checks are required because accountHolds
+    // with ZeroIfFrozen/ZeroIfUnauthorized only implicitly rejects
+    // non-zero amounts. A zero-amount convert would bypass those implicit
+    // checks, allowing frozen or unauthorized accounts to register ElGamal
+    // keys and initialize confidential balance fields.
+
+    // Check lock
+    if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
+        return ter;
+
+    // Check auth
+    if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
+        return ter;
+
+    auto const mptAmount =
+        STAmount(MPTAmount{static_cast(amount)}, mptIssue);
+    if (accountHolds(
+            ctx.view,
+            account,
+            mptIssue,
+            FreezeHandling::ZeroIfFrozen,
+            AuthHandling::ZeroIfUnauthorized,
+            ctx.j) < mptAmount)
+    {
+        return tecINSUFFICIENT_FUNDS;
+    }
+
+    auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderEncryptionKey);
+    auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderEncryptionKey);
+
+    // must have pk to convert
+    if (!hasHolderKeyOnLedger && !hasHolderKeyInTx)
+        return tecNO_PERMISSION;
+
+    // can't update if there's already a pk
+    if (hasHolderKeyOnLedger && hasHolderKeyInTx)
+        return tecDUPLICATE;
+
+    // Run all verifications before returning any error to prevent timing attacks
+    // that could reveal which proof failed.
+    bool valid = true;
+
+    Slice holderPubKey;
+    if (hasHolderKeyInTx)
+    {
+        holderPubKey = ctx.tx[sfHolderEncryptionKey];
+
+        auto const contextHash =
+            getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value());
+
+        if (auto const ter = verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash);
+            !isTesSuccess(ter))
+        {
+            valid = false;
+        }
+    }
+    else
+    {
+        holderPubKey = (*sleMptoken)[sfHolderEncryptionKey];
+    }
+
+    std::optional auditor;
+    if (hasAuditor)
+    {
+        auditor.emplace(
+            ConfidentialRecipient{
+                .publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
+                .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
+            });
+    }
+
+    auto const blindingFactor = ctx.tx[sfBlindingFactor];
+    if (auto const ter = verifyRevealedAmount(
+            amount,
+            Slice(blindingFactor.data(), blindingFactor.size()),
+            {
+                .publicKey = holderPubKey,
+                .encryptedAmount = ctx.tx[sfHolderEncryptedAmount],
+            },
+            {
+                .publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
+                .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
+            },
+            auditor);
+        !isTesSuccess(ter))
+    {
+        valid = false;
+    }
+
+    if (!valid)
+        return tecBAD_PROOF;
+
+    return tesSUCCESS;
+}
+
+TER
+ConfidentialMPTConvert::doApply()
+{
+    auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
+
+    auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
+    if (!sleMptoken)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
+    if (!sleIssuance)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const amtToConvert = ctx_.tx[sfMPTAmount];
+    auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
+
+    if (ctx_.tx.isFieldPresent(sfHolderEncryptionKey))
+        (*sleMptoken)[sfHolderEncryptionKey] = ctx_.tx[sfHolderEncryptionKey];
+
+    // Converting decreases regular balance and increases confidential outstanding.
+    // The confidential outstanding tracks total tokens in confidential form globally.
+    auto const currentCOA = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0);
+    if (amtToConvert > kMaxMpTokenAmount - currentCOA)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    (*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
+    (*sleIssuance)[sfConfidentialOutstandingAmount] = currentCOA + amtToConvert;
+
+    auto const holderEc = ctx_.tx[sfHolderEncryptedAmount];
+    auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
+    auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
+
+    // Two cases for Convert:
+    // 1. Holder already has confidential balances -> homomorphically add to inbox
+    // 2. First-time convert -> initialize all confidential balance fields
+    if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
+        sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
+        sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
+    {
+        // Case 1: Add to existing inbox balance (holder will merge later)
+        {
+            auto sum = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox]);
+            if (!sum)
+            {
+                // LCOV_EXCL_START
+                JLOG(ctx_.journal.error())
+                    << "ConfidentialMPTConvert failed homomorphic add for holder inbox.";
+                return tecINTERNAL;
+                // LCOV_EXCL_STOP
+            }
+
+            (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*sum);
+        }
+
+        // homomorphically add issuer's encrypted balance
+        {
+            auto sum = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance]);
+            if (!sum)
+            {
+                // LCOV_EXCL_START
+                JLOG(ctx_.journal.error())
+                    << "ConfidentialMPTConvert failed homomorphic add for issuer balance.";
+                return tecINTERNAL;
+                // LCOV_EXCL_STOP
+            }
+
+            (*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*sum);
+        }
+
+        // homomorphically add auditor's encrypted balance
+        if (auditorEc)
+        {
+            if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
+                return tecINTERNAL;  // LCOV_EXCL_LINE
+
+            auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
+            if (!sum)
+            {
+                // LCOV_EXCL_START
+                JLOG(ctx_.journal.error())
+                    << "ConfidentialMPTConvert failed homomorphic add for auditor balance.";
+                return tecINTERNAL;
+                // LCOV_EXCL_STOP
+            }
+
+            (*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*sum);
+        }
+    }
+    else if (
+        !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
+        !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
+        !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) &&
+        !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
+    {
+        // Case 2: First-time convert - initialize all confidential fields
+        (*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
+        (*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
+        (*sleMptoken)[sfConfidentialBalanceVersion] = 0;
+
+        if (auditorEc)
+            (*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
+
+        // Spending balance starts at zero. Must use canonical zero encryption
+        // (deterministic ciphertext) so the ledger state is reproducible.
+        auto zeroBalance = encryptCanonicalZeroAmount(
+            (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
+
+        if (!zeroBalance)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
+    }
+    else
+    {
+        // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
+        // exist together
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    view().update(sleIssuance);
+    view().update(sleMptoken);
+    return tesSUCCESS;
+}
+
+void
+ConfidentialMPTConvert::visitInvariantEntry(
+    bool,
+    std::shared_ptr const&,
+    std::shared_ptr const&)
+{
+}
+
+bool
+ConfidentialMPTConvert::finalizeInvariants(
+    STTx const&,
+    TER,
+    XRPAmount,
+    ReadView const&,
+    beast::Journal const&)
+{
+    return true;
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp
new file mode 100644
index 0000000000..d6fed78833
--- /dev/null
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp
@@ -0,0 +1,319 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+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;
+
+    if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > kMaxMpTokenAmount)
+        return temBAD_AMOUNT;
+
+    if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]))
+        return temMALFORMED;
+
+    // check encrypted amount format after the above basic checks
+    // this check is more expensive so put it at the end
+    if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
+        return res;
+
+    // ConvertBack proof = compact sigma proof (128 bytes) + single bulletproof (688 bytes)
+    if (ctx.tx[sfZKProof].size() != kEcConvertBackProofLength)
+        return temMALFORMED;
+
+    return tesSUCCESS;
+}
+
+XRPAmount
+ConfidentialMPTConvertBack::calculateBaseFee(ReadView const& view, STTx const& tx)
+{
+    return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
+}
+
+/**
+ * Verifies the cryptographic proofs for a ConvertBack transaction.
+ *
+ * This function verifies three proofs:
+ * 1. Revealed amount proof: verifies the encrypted amounts (holder, issuer,
+ *    auditor) all encrypt the same revealed amount using the blinding factor.
+ * 2. Pedersen linkage proof: verifies the balance commitment is derived from
+ *    the holder's encrypted spending balance.
+ * 3. Bulletproof (range proof): verifies the remaining balance (balance - amount)
+ *    is non-negative, preventing overdrafts.
+ *
+ * All proofs are verified before returning any error to prevent timing attacks.
+ */
+static TER
+verifyProofs(
+    STTx const& tx,
+    std::shared_ptr const& issuance,
+    std::shared_ptr const& mptoken)
+{
+    if (!mptoken->isFieldPresent(sfHolderEncryptionKey))
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
+    auto const account = tx[sfAccount];
+    auto const amount = tx[sfMPTAmount];
+    auto const blindingFactor = tx[sfBlindingFactor];
+    auto const holderPubKey = (*mptoken)[sfHolderEncryptionKey];
+
+    auto const contextHash = getConvertBackContextHash(
+        account,
+        mptIssuanceID,
+        tx.getSeqProxy().value(),
+        (*mptoken)[~sfConfidentialBalanceVersion].value_or(0));
+
+    // Prepare Auditor Info
+    std::optional auditor;
+    bool const hasAuditor = issuance->isFieldPresent(sfAuditorEncryptionKey);
+    if (hasAuditor)
+    {
+        auditor.emplace(
+            ConfidentialRecipient{
+                .publicKey = (*issuance)[sfAuditorEncryptionKey],
+                .encryptedAmount = tx[sfAuditorEncryptedAmount],
+            });
+    }
+
+    // Run all verifications before returning any error to prevent timing attacks
+    // that could reveal which proof failed.
+    bool valid = true;
+
+    if (auto const ter = verifyRevealedAmount(
+            amount,
+            Slice(blindingFactor.data(), blindingFactor.size()),
+            {
+                .publicKey = holderPubKey,
+                .encryptedAmount = tx[sfHolderEncryptedAmount],
+            },
+            {
+                .publicKey = (*issuance)[sfIssuerEncryptionKey],
+                .encryptedAmount = tx[sfIssuerEncryptedAmount],
+            },
+            auditor);
+        !isTesSuccess(ter))
+    {
+        valid = false;
+    }
+
+    if (auto const ter = verifyConvertBackProof(
+            tx[sfZKProof],
+            holderPubKey,
+            (*mptoken)[sfConfidentialBalanceSpending],
+            tx[sfBalanceCommitment],
+            amount,
+            contextHash);
+        !isTesSuccess(ter))
+    {
+        valid = false;
+    }
+
+    if (!valid)
+        return tecBAD_PROOF;
+
+    return tesSUCCESS;
+}
+
+TER
+ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
+{
+    auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
+    auto const account = ctx.tx[sfAccount];
+    auto const amount = ctx.tx[sfMPTAmount];
+
+    // ensure that issuance exists
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
+    if (!sleIssuance)
+        return tecOBJECT_NOT_FOUND;
+
+    if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) ||
+        !sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
+        return tecNO_PERMISSION;
+
+    bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
+    bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
+
+    // tx must include auditor ciphertext if the issuance has enabled
+    // auditing
+    if (requiresAuditor && !hasAuditor)
+        return tecNO_PERMISSION;
+
+    // if auditing is not supported then user should not upload auditor
+    // ciphertext
+    if (!requiresAuditor && hasAuditor)
+        return tecNO_PERMISSION;
+
+    // already checked in preflight, but should also check that issuer on
+    // the issuance isn't the account either
+    if (sleIssuance->getAccountID(sfIssuer) == account)
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
+    if (!sleMptoken)
+        return tecOBJECT_NOT_FOUND;
+
+    if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) ||
+        !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
+        !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance))
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // Sanity check: holder's MPToken must have auditor balance field if auditing
+    // is enabled
+    if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+
+    // if the total circulating confidential balance is smaller than what the
+    // holder is trying to convert back, we know for sure this txn should
+    // fail
+    if ((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) < amount)
+        return tecINSUFFICIENT_FUNDS;
+
+    // Check lock
+    MPTIssue const mptIssue(mptIssuanceID);
+    if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
+        return ter;
+
+    // Check auth
+    if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
+        return ter;
+
+    if (auto const res = verifyProofs(ctx.tx, sleIssuance, sleMptoken); !isTesSuccess(res))
+        return res;
+
+    return tesSUCCESS;
+}
+
+TER
+ConfidentialMPTConvertBack::doApply()
+{
+    auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
+
+    auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
+    if (!sleMptoken)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
+    if (!sleIssuance)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
+    auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
+
+    // Converting back increases regular balance and decreases confidential
+    // outstanding. This is the inverse of Convert.
+    if (amt > kMaxMpTokenAmount - amtToConvertBack)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    (*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
+
+    auto const coa = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0);
+    if (coa < amtToConvertBack)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    (*sleIssuance)[sfConfidentialOutstandingAmount] = coa - amtToConvertBack;
+
+    std::optional const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
+
+    // homomorphically subtract holder's encrypted balance
+    {
+        auto res = homomorphicSubtract(
+            (*sleMptoken)[sfConfidentialBalanceSpending], ctx_.tx[sfHolderEncryptedAmount]);
+        if (!res)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTConvertBack failed homomorphic subtract for holder spending "
+                   "balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*res);
+    }
+
+    // homomorphically subtract issuer's encrypted balance
+    {
+        auto res = homomorphicSubtract(
+            (*sleMptoken)[sfIssuerEncryptedBalance], ctx_.tx[sfIssuerEncryptedAmount]);
+        if (!res)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTConvertBack failed homomorphic subtract for issuer balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*res);
+    }
+
+    if (auditorEc)
+    {
+        auto res = homomorphicSubtract(
+            (*sleMptoken)[sfAuditorEncryptedBalance], ctx_.tx[sfAuditorEncryptedAmount]);
+        if (!res)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTConvertBack failed homomorphic subtract for auditor balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*res);
+    }
+
+    incrementConfidentialVersion(*sleMptoken);
+
+    view().update(sleIssuance);
+    view().update(sleMptoken);
+    return tesSUCCESS;
+}
+
+void
+ConfidentialMPTConvertBack::visitInvariantEntry(
+    bool,
+    std::shared_ptr const&,
+    std::shared_ptr const&)
+{
+}
+
+bool
+ConfidentialMPTConvertBack::finalizeInvariants(
+    STTx const&,
+    TER,
+    XRPAmount,
+    ReadView const&,
+    beast::Journal const&)
+{
+    return true;
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp
new file mode 100644
index 0000000000..02c759c521
--- /dev/null
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp
@@ -0,0 +1,150 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+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;
+
+    return tesSUCCESS;
+}
+
+XRPAmount
+ConfidentialMPTMergeInbox::calculateBaseFee(ReadView const& view, STTx const& tx)
+{
+    return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
+}
+
+TER
+ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
+{
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
+    if (!sleIssuance)
+        return tecOBJECT_NOT_FOUND;
+
+    if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
+        return tecNO_PERMISSION;
+
+    // already checked in preflight, but should also check that issuer on the
+    // issuance isn't the account either
+    if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const sleMptoken =
+        ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
+    if (!sleMptoken)
+        return tecOBJECT_NOT_FOUND;
+
+    if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
+        !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
+        !sleMptoken->isFieldPresent(sfHolderEncryptionKey))
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // Check lock
+    auto const account = ctx.tx[sfAccount];
+    MPTIssue const mptIssue(ctx.tx[sfMPTokenIssuanceID]);
+    if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
+        return ter;
+
+    // Check auth
+    if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
+        return ter;
+
+    return tesSUCCESS;
+}
+
+TER
+ConfidentialMPTMergeInbox::doApply()
+{
+    auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
+    auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
+    if (!sleMptoken)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    // sanity check
+    if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
+        !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
+        !sleMptoken->isFieldPresent(sfHolderEncryptionKey))
+    {
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    // Merge inbox into spending: spending = spending + inbox
+    // This allows holder to use received funds. Without merging, incoming
+    // transfers sit in inbox and cannot be spent or converted back.
+    auto sum = homomorphicAdd(
+        (*sleMptoken)[sfConfidentialBalanceSpending], (*sleMptoken)[sfConfidentialBalanceInbox]);
+    if (!sum)
+    {
+        // LCOV_EXCL_START
+        JLOG(ctx_.journal.error())
+            << "ConfidentialMPTMergeInbox failed homomorphic add for inbox merge.";
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
+
+    (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*sum);
+
+    // Reset inbox to encrypted zero. Must use canonical zero encryption
+    // (deterministic ciphertext) so the ledger state is reproducible.
+    auto zeroEncryption =
+        encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
+
+    if (!zeroEncryption)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption);
+
+    incrementConfidentialVersion(*sleMptoken);
+
+    view().update(sleMptoken);
+    return tesSUCCESS;
+}
+
+void
+ConfidentialMPTMergeInbox::visitInvariantEntry(
+    bool,
+    std::shared_ptr const&,
+    std::shared_ptr const&)
+{
+}
+
+bool
+ConfidentialMPTMergeInbox::finalizeInvariants(
+    STTx const&,
+    TER,
+    XRPAmount,
+    ReadView const&,
+    beast::Journal const&)
+{
+    return true;
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
new file mode 100644
index 0000000000..302b7d239b
--- /dev/null
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
@@ -0,0 +1,445 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+bool
+ConfidentialMPTSend::checkExtraFeatures(PreflightContext const& ctx)
+{
+    return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
+}
+
+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();
+
+    // ConfidentialMPTSend only allows holder to holder, holder to second account,
+    // and second account to holder transfers. So issuer cannot be the sender.
+    if (account == issuer)
+        return temMALFORMED;
+
+    // Can not send to self
+    if (account == ctx.tx[sfDestination])
+        return temMALFORMED;
+
+    // Issuer cannot be the destination
+    if (ctx.tx[sfDestination] == issuer)
+        return temMALFORMED;
+
+    // Check the length of the encrypted amounts
+    if (ctx.tx[sfSenderEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
+        ctx.tx[sfDestinationEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
+        ctx.tx[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
+    {
+        return temBAD_CIPHERTEXT;
+    }
+
+    bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
+    if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength)
+        return temBAD_CIPHERTEXT;
+
+    // Check the length of the ZKProof (fixed size regardless of recipient count)
+    if (ctx.tx[sfZKProof].length() != kEcSendProofLength)
+        return temMALFORMED;
+
+    // Check the Pedersen commitments are valid
+    if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]) ||
+        !isValidCompressedECPoint(ctx.tx[sfAmountCommitment]))
+    {
+        return temMALFORMED;
+    }
+
+    // Check the encrypted amount formats, this is more expensive so put it at
+    // the end
+    if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
+        !isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) ||
+        !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
+    {
+        return temBAD_CIPHERTEXT;
+    }
+
+    if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
+        return temBAD_CIPHERTEXT;
+
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
+        return err;
+
+    return tesSUCCESS;
+}
+
+XRPAmount
+ConfidentialMPTSend::calculateBaseFee(ReadView const& view, STTx const& tx)
+{
+    return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier);
+}
+
+namespace detail {
+
+static TER
+verifySendProofs(
+    PreclaimContext const& ctx,
+    std::shared_ptr const& sleSenderMPToken,
+    std::shared_ptr const& sleDestinationMPToken,
+    std::shared_ptr const& sleIssuance)
+{
+    // Sanity check
+    if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
+
+    std::optional auditor;
+    if (hasAuditor)
+    {
+        auditor.emplace(
+            ConfidentialRecipient{
+                .publicKey = (*sleIssuance)[sfAuditorEncryptionKey],
+                .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount],
+            });
+    }
+
+    auto const contextHash = getSendContextHash(
+        ctx.tx[sfAccount],
+        ctx.tx[sfMPTokenIssuanceID],
+        ctx.tx.getSeqProxy().value(),
+        ctx.tx[sfDestination],
+        (*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
+
+    return verifySendProof(
+        ctx.tx[sfZKProof],
+        {
+            .publicKey = (*sleSenderMPToken)[sfHolderEncryptionKey],
+            .encryptedAmount = ctx.tx[sfSenderEncryptedAmount],
+        },
+        {
+            .publicKey = (*sleDestinationMPToken)[sfHolderEncryptionKey],
+            .encryptedAmount = ctx.tx[sfDestinationEncryptedAmount],
+        },
+        {
+            .publicKey = (*sleIssuance)[sfIssuerEncryptionKey],
+            .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount],
+        },
+        auditor,
+        (*sleSenderMPToken)[sfConfidentialBalanceSpending],
+        ctx.tx[sfAmountCommitment],
+        ctx.tx[sfBalanceCommitment],
+        contextHash);
+}
+
+}  // namespace detail
+
+TER
+ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
+{
+    // Check if sender account exists
+    auto const account = ctx.tx[sfAccount];
+    if (!ctx.view.exists(keylet::account(account)))
+        return terNO_ACCOUNT;
+
+    // Check if destination account exists
+    auto const destination = ctx.tx[sfDestination];
+    auto const sleDst = ctx.view.read(keylet::account(destination));
+    if (!sleDst)
+        return tecNO_TARGET;
+
+    // Check destination tag
+    if (((sleDst->getFlags() & lsfRequireDestTag) != 0u) &&
+        !ctx.tx.isFieldPresent(sfDestinationTag))
+    {
+        return tecDST_TAG_NEEDED;
+    }
+
+    // Check if MPT issuance exists
+    auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
+    auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
+    if (!sleIssuance)
+        return tecOBJECT_NOT_FOUND;
+
+    // Check if the issuance allows transfer
+    if (!sleIssuance->isFlag(lsfMPTCanTransfer))
+        return tecNO_AUTH;
+
+    // Check if issuance allows confidential transfer
+    if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
+        return tecNO_PERMISSION;
+
+    // Sanity check: transfer fee must be 0 for confidential MPTs. This should
+    // be unreachable in valid ledger state because MPTokenIssuanceCreate and
+    // MPTokenIssuanceSet enforce it.
+    if ((*sleIssuance)[~sfTransferFee].value_or(0) > 0)
+        return tecNO_PERMISSION;
+
+    // Check if issuance has issuer ElGamal public key
+    if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
+        return tecNO_PERMISSION;
+
+    bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
+    bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
+
+    // Tx must include auditor ciphertext if the issuance has enabled
+    // auditing, and must not include it if auditing is not enabled
+    if (requiresAuditor != hasAuditor)
+        return tecNO_PERMISSION;
+
+    // Sanity check: issuer isn't the sender
+    if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+
+    // Check sender's MPToken existence
+    auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
+    if (!sleSenderMPToken)
+        return tecOBJECT_NOT_FOUND;
+
+    // Check sender's MPToken has necessary fields for confidential send
+    if (!sleSenderMPToken->isFieldPresent(sfHolderEncryptionKey) ||
+        !sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
+        !sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // Check destination's MPToken existence
+    auto const sleDestinationMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, destination));
+    if (!sleDestinationMPToken)
+        return tecOBJECT_NOT_FOUND;
+
+    // Check destination's MPToken has necessary fields for confidential send
+    if (!sleDestinationMPToken->isFieldPresent(sfHolderEncryptionKey) ||
+        !sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
+        !sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // Sanity check: Both MPTokens' auditor fields must be present if auditing
+    // is enabled
+    if (requiresAuditor &&
+        (!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) ||
+         !sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance)))
+    {
+        return tefINTERNAL;  // LCOV_EXCL_LINE
+    }
+
+    // Check lock
+    MPTIssue const mptIssue(mptIssuanceID);
+    if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
+        return ter;
+
+    if (auto const ter = checkFrozen(ctx.view, destination, mptIssue); !isTesSuccess(ter))
+        return ter;
+
+    // Check auth
+    if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
+        return ter;
+
+    if (auto const ter = requireAuth(ctx.view, mptIssue, destination); !isTesSuccess(ter))
+        return ter;
+
+    if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
+        !isTesSuccess(err))
+        return err;
+
+    // Check deposit preauth before the expensive ZK proof verification.
+    // Uses read-only view.
+    auto const preauthErr =
+        checkDepositPreauth(ctx.tx, ctx.view, account, destination, sleDst, ctx.j);
+    if (!isTesSuccess(preauthErr))
+        return preauthErr;
+
+    return detail::verifySendProofs(ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance);
+}
+
+TER
+ConfidentialMPTSend::doApply()
+{
+    auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
+    auto const destination = ctx_.tx[sfDestination];
+
+    auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
+    auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
+    auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
+
+    auto const sleDestAcct = view().read(keylet::account(destination));
+
+    if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    // Deposit preauth authorization was already verified in preclaim.
+    // Remove any expired credentials.
+    if (auto err = cleanupExpiredCredentials(ctx_.tx, ctx_.view(), ctx_.journal);
+        !isTesSuccess(err))
+        return err;
+
+    auto const senderEc = ctx_.tx[sfSenderEncryptedAmount];
+    auto const destEc = ctx_.tx[sfDestinationEncryptedAmount];
+    auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
+    auto const proof = ctx_.tx[sfZKProof];
+    Slice const sendChallenge{proof.data(), kEcBlindingFactorLength};
+
+    auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
+
+    // Subtract from sender's spending balance
+    {
+        auto const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending];
+        auto newSpending = homomorphicSubtract(curSpending, senderEc);
+        if (!newSpending)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed homomorphic subtract for sender spending balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleSenderMPToken)[sfConfidentialBalanceSpending] = std::move(*newSpending);
+    }
+
+    // Subtract from issuer's balance
+    {
+        auto const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance];
+        auto newIssuerEnc = homomorphicSubtract(curIssuerEnc, issuerEc);
+        if (!newIssuerEnc)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed homomorphic subtract for sender issuer balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleSenderMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
+    }
+
+    // Subtract from auditor's balance if present
+    if (auditorEc)
+    {
+        auto const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance];
+        auto newAuditorEnc = homomorphicSubtract(curAuditorEnc, *auditorEc);
+        if (!newAuditorEnc)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed homomorphic subtract for sender auditor balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleSenderMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
+    }
+
+    // Add to destination's inbox balance
+    {
+        auto rerandomizedDestEc = rerandomizeCiphertext(
+            destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge);
+        if (!rerandomizedDestEc)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
+        auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc);
+        if (!newInbox)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed homomorphic add for destination inbox.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleDestinationMPToken)[sfConfidentialBalanceInbox] = std::move(*newInbox);
+    }
+
+    // Add to issuer's balance
+    {
+        auto rerandomizedIssuerEc =
+            rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge);
+        if (!rerandomizedIssuerEc)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
+        auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc);
+        if (!newIssuerEnc)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed homomorphic add for destination issuer balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleDestinationMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc);
+    }
+
+    // Add to auditor's balance if present
+    if (auditorEc)
+    {
+        auto rerandomizedAuditorEc = rerandomizeCiphertext(
+            *auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge);
+        if (!rerandomizedAuditorEc)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
+        auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc);
+        if (!newAuditorEnc)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed homomorphic add for destination auditor balance.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        (*sleDestinationMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc);
+    }
+
+    // increment sender version only; receiver version is not modified by incoming sends
+    incrementConfidentialVersion(*sleSenderMPToken);
+
+    view().update(sleSenderMPToken);
+    view().update(sleDestinationMPToken);
+    return tesSUCCESS;
+}
+
+void
+ConfidentialMPTSend::visitInvariantEntry(
+    bool,
+    std::shared_ptr const&,
+    std::shared_ptr const&)
+{
+}
+
+bool
+ConfidentialMPTSend::finalizeInvariants(
+    STTx const&,
+    TER,
+    XRPAmount,
+    ReadView const&,
+    beast::Journal const&)
+{
+    return true;
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
index e1d9daaada..6db37515cb 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
@@ -83,6 +83,25 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
             if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked))
                 return tecNO_PERMISSION;
 
+            if (ctx.view.rules().enabled(featureConfidentialTransfer))
+            {
+                auto const sleMptIssuance =
+                    ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
+
+                // if there still existing encrypted balances of MPT in
+                // circulation
+                if (sleMptIssuance &&
+                    (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) != 0)
+                {
+                    // this MPT still has encrypted balance, since we don't know
+                    // if it's non-zero or not, we won't allow deletion of
+                    // MPToken
+                    if (sleMpt->isFieldPresent(sfConfidentialBalanceInbox) ||
+                        sleMpt->isFieldPresent(sfConfidentialBalanceSpending))
+                        return tecHAS_OBLIGATIONS;
+                }
+            }
+
             return tesSUCCESS;
         }
 
diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp
index 68956a533d..e30127b688 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp
@@ -37,7 +37,15 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
     if (ctx.tx.isFieldPresent(sfMutableFlags) && !ctx.rules.enabled(featureDynamicMPT))
         return false;
 
-    return true;
+    if (ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance) &&
+        !ctx.rules.enabled(featureConfidentialTransfer))
+        return false;
+
+    // can not set tmfMPTCannotEnableCanHoldConfidentialBalance without featureConfidentialTransfer
+    auto const mutableFlags = ctx.tx[~sfMutableFlags];
+    return !mutableFlags ||
+        ((*mutableFlags & tmfMPTCannotEnableCanHoldConfidentialBalance) == 0u) ||
+        ctx.rules.enabled(featureConfidentialTransfer);
 }
 
 std::uint32_t
@@ -70,6 +78,10 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
         // must also be set.
         if (fee > 0u && !ctx.tx.isFlag(tfMPTCanTransfer))
             return temMALFORMED;
+
+        // Confidential amounts are encrypted so transfer rate is disallowed.
+        if (fee > 0u && ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance))
+            return temBAD_TRANSFER_FEE;
     }
 
     if (auto const domain = ctx.tx[~sfDomainID])
diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
index e200c9762a..d526251069 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -74,11 +75,27 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
     auto const metadata = ctx.tx[~sfMPTokenMetadata];
     auto const transferFee = ctx.tx[~sfTransferFee];
     auto const isMutate = mutableFlags || metadata || transferFee;
+    auto const hasIssuerElGamalKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey);
+    auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
+    auto const txFlags = ctx.tx.getFlags();
+
+    bool const enablePrivacy =
+        mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u;
+
+    auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID);
+    auto const hasHolder = ctx.tx.isFieldPresent(sfHolder);
 
     if (isMutate && !ctx.rules.enabled(featureDynamicMPT))
         return temDISABLED;
 
-    if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
+    if ((hasIssuerElGamalKey || hasAuditorElGamalKey || enablePrivacy) &&
+        !ctx.rules.enabled(featureConfidentialTransfer))
+        return temDISABLED;
+
+    if (hasDomain && hasHolder)
+        return temMALFORMED;
+
+    if (enablePrivacy && hasHolder)
         return temMALFORMED;
 
     // fails if both flags are set
@@ -90,10 +107,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
     if (holderID && accountID == holderID)
         return temMALFORMED;
 
-    if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT))
+    if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT) ||
+        ctx.rules.enabled(featureConfidentialTransfer))
     {
         // Is this transaction actually changing anything ?
-        if (ctx.tx.getFlags() == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
+        if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && !hasAuditorElGamalKey &&
+            !isMutate)
             return temMALFORMED;
     }
 
@@ -110,6 +129,9 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
         if (transferFee && *transferFee > kMaxTransferFee)
             return temBAD_TRANSFER_FEE;
 
+        if (transferFee && *transferFee > 0u && enablePrivacy)
+            return temBAD_TRANSFER_FEE;
+
         if (metadata && metadata->length() > kMaxMpTokenMetadataLength)
             return temMALFORMED;
 
@@ -120,6 +142,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
         }
     }
 
+    if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
+        return temMALFORMED;
+
+    if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
+        return temMALFORMED;
+
+    if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
+        return temMALFORMED;
+
+    if (hasAuditorElGamalKey && !isValidCompressedECPoint(ctx.tx[sfAuditorEncryptionKey]))
+        return temMALFORMED;
+
     return tesSUCCESS;
 }
 
@@ -181,12 +215,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
         return currentMutableFlags & mutableFlag;
     };
 
-    if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
+    auto const mutableFlags = ctx.tx[~sfMutableFlags];
+    // Whether the transaction is enabling confidential amounts.
+    bool const enablesConfidentialAmount =
+        mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u;
+    if (mutableFlags)
     {
         if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags, &isMutableFlag](auto const& f) {
                 return !isMutableFlag(f.canEnableFlag) && ((*mutableFlags & f.setFlag) != 0u);
             }))
             return tecNO_PERMISSION;
+
+        if (enablesConfidentialAmount &&
+            isMutableFlag(lsmfMPTCannotEnableCanHoldConfidentialBalance))
+            return tecNO_PERMISSION;
     }
 
     if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))
@@ -201,10 +243,55 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
         if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer))
             return tecNO_PERMISSION;
 
+        // Cannot set a non-zero TransferFee on an issuance that has confidential
+        // transfer enabled
+        if (fee > 0u && sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance))
+            return tecNO_PERMISSION;
+
         if (!isMutableFlag(lsmfMPTCanMutateTransferFee))
             return tecNO_PERMISSION;
     }
 
+    // cannot update issuer public key
+    if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
+        sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // cannot update auditor public key
+    if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
+        sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
+    {
+        return tecNO_PERMISSION;  // LCOV_EXCL_LINE
+    }
+
+    if (enablesConfidentialAmount && sleMptIssuance->isFieldPresent(sfTransferFee) &&
+        (*sleMptIssuance)[sfTransferFee] > 0u)
+        return tecNO_PERMISSION;
+
+    // Encryption keys can only be set if confidential amounts are already
+    // enabled on the issuance OR if the transaction is enabling it
+    if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
+        !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount)
+    {
+        return tecNO_PERMISSION;
+    }
+
+    if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
+        !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount)
+    {
+        return tecNO_PERMISSION;
+    }
+
+    // cannot upload key if there's circulating supply of COA
+    if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
+         ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialAmount) &&
+        (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
+    {
+        return tecNO_PERMISSION;  // LCOV_EXCL_LINE
+    }
+
     return tesSUCCESS;
 }
 
@@ -249,6 +336,9 @@ MPTokenIssuanceSet::doApply()
                 flagsOut |= f.ledgerFlag;
             }
         }
+
+        if ((mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u)
+            flagsOut |= lsfMPTCanHoldConfidentialBalance;
     }
 
     if (flagsIn != flagsOut)
@@ -300,6 +390,26 @@ MPTokenIssuanceSet::doApply()
         }
     }
 
+    if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
+    {
+        // This is enforced in preflight.
+        XRPL_ASSERT(
+            sle->getType() == ltMPTOKEN_ISSUANCE,
+            "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
+
+        sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
+    }
+
+    if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
+    {
+        // This is enforced in preflight.
+        XRPL_ASSERT(
+            sle->getType() == ltMPTOKEN_ISSUANCE,
+            "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
+
+        sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
+    }
+
     view().update(sle);
 
     return tesSUCCESS;
diff --git a/src/test/app/ConfidentialTransferExtended_test.cpp b/src/test/app/ConfidentialTransferExtended_test.cpp
new file mode 100644
index 0000000000..0aee7516c9
--- /dev/null
+++ b/src/test/app/ConfidentialTransferExtended_test.cpp
@@ -0,0 +1,2595 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
+{
+    void
+    testSendDepositPreauth(FeatureBitset features)
+    {
+        testcase("Send deposit preauth");
+        using namespace test::jtx;
+
+        // When an account enables lsfDepositAuth (via asfDepositAuth flag),
+        // it requires explicit authorization before accepting incoming payments.
+        //
+        // There are two authorization mechanisms:
+        //
+        // 1. DIRECT ACCOUNT AUTHORIZATION (deposit::auth)
+        //    - Bob directly authorizes Carol: deposit::auth(bob, carol)
+        //    - Simple 1-to-1 trust relationship
+        //    - Carol can send to Bob without credentials
+        //
+        // 2. CREDENTIAL-BASED AUTHORIZATION (deposit::authCredentials)
+        //    - A trusted third party (dpIssuer) issues credentials
+        //    - Bob authorizes a credential TYPE from an issuer
+        //    - Anyone holding that credential can send to Bob
+        //    - Requires sender to include credential ID in transaction
+
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dpIssuer("dpIssuer");
+        char const credType[] = "KYC_VERIFIED";
+
+        // Create and accept credential for an account
+        auto createCredential = [&](Env& env, Account const& subject) -> std::string {
+            env(credentials::create(subject, dpIssuer, credType));
+            env.close();
+            env(credentials::accept(subject, dpIssuer, credType));
+            env.close();
+            auto const jv = credentials::ledgerEntry(env, subject, dpIssuer, credType);
+            return jv[jss::result][jss::index].asString();
+        };
+
+        // TEST 1: Direct Account Authorization
+        {
+            Env env(*this, features);
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+            env(fset(bob, asfDepositAuth));
+            env.close();
+
+            // Carol cannot send to Bob without authorization
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+
+            // Bob directly authorizes Carol
+            env(deposit::auth(bob, carol));
+            env.close();
+
+            // Now Carol can send to Bob
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+            });
+            mpt.mergeInbox({
+                .account = bob,
+            });
+
+            // Bob revokes Carol's authorization
+            env(deposit::unauth(bob, carol));
+            env.close();
+
+            // Carol can no longer send to Bob
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // TEST 2: Credential-Based Authorization
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+            env(fset(bob, asfDepositAuth));
+            env.close();
+
+            auto const credIdx = createCredential(env, carol);
+
+            // Carol cannot send yet - Bob hasn't authorized this credential type
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecNO_PERMISSION,
+            });
+
+            // Bob authorizes the credential type from dpIssuer
+            env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
+            env.close();
+
+            // Carol still cannot send without including credential
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+
+            // Carol CAN send when including her credential
+            mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}});
+            mpt.mergeInbox({
+                .account = bob,
+            });
+        }
+
+        // TEST 3: Direct Auth Takes Precedence Over Credentials
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+            env(fset(bob, asfDepositAuth));
+            env.close();
+
+            auto const credIdx = createCredential(env, carol);
+
+            // Bob directly authorizes Carol (no credential needed)
+            env(deposit::auth(bob, carol));
+            env.close();
+
+            // Carol can send without credentials (direct auth)
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+            });
+            mpt.mergeInbox({
+                .account = bob,
+            });
+
+            // Carol can also send WITH credentials (still works)
+            mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}});
+            mpt.mergeInbox({
+                .account = bob,
+            });
+
+            // Bob revokes direct authorization
+            env(deposit::unauth(bob, carol));
+            env.close();
+
+            // Carol cannot send without credentials anymore
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+
+            // But credential-based auth not set up, so this also fails
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecNO_PERMISSION,
+            });
+
+            // Bob authorizes the credential type
+            env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
+            env.close();
+
+            // Now Carol can send with credentials
+            mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}});
+        }
+
+        auto const expireTime = 30;
+
+        // Lambda function that returns the credential index after creating a
+        // credential that expires shortly after the current ledger time.
+        auto createExpiringCredential = [&](Env& env, Account const& subject) -> std::string {
+            auto jv = credentials::create(subject, dpIssuer, credType);
+            auto const expiry =
+                env.current()->header().parentCloseTime.time_since_epoch().count() + expireTime;
+            jv[sfExpiration.jsonName] = expiry;
+            env(jv);
+            env.close();
+            env(credentials::accept(subject, dpIssuer, credType));
+            env.close();
+            auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType);
+            return credentials[jss::result][jss::index].asString();
+        };
+
+        auto credentialDeleted = [&](Env& env, Account const& subject) -> bool {
+            auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType);
+            return credentials[jss::result].isMember(jss::error) &&
+                credentials[jss::result][jss::error] == "entryNotFound";
+        };
+
+        // TEST 4: Expired credential with matching depositPreauth entry.
+        // checkDepositPreauth in preclaim returns tesSUCCESS (the expired
+        // credential still exists and matches the depositPreauth key), so ZK
+        // proofs run. cleanupExpiredCredentials in doApply then removes the
+        // expired credential and returns tecEXPIRED.
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+            env(fset(bob, asfDepositAuth));
+            env.close();
+
+            auto const credIdx = createExpiringCredential(env, carol);
+
+            // Bob authorizes carol's credential type
+            env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
+            env.close();
+
+            // Advance ledger past credential expiration
+            env.close(std::chrono::seconds(expireTime));
+
+            // Send fails with tecEXPIRED; the expired credential is cleaned up
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecEXPIRED,
+            });
+            env.close();
+
+            BEAST_EXPECT(credentialDeleted(env, carol));
+        }
+
+        // TEST 5: Expired credential, destination has no depositAuth.
+        // checkDepositPreauth in preclaim returns tesSUCCESS even with expired credentials,
+        // because we want to keep the checkDepositPreauth part before the expensive proof
+        // verification. cleanupExpiredCredentials in doApply removes the expired credential and
+        // returns tecEXPIRED.
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            auto const credIdx = createExpiringCredential(env, carol);
+
+            // Advance ledger past credential expiration
+            env.close(std::chrono::seconds(expireTime));
+
+            // Send fails with tecEXPIRED; the expired credential is cleaned up
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecEXPIRED,
+            });
+            env.close();
+
+            BEAST_EXPECT(credentialDeleted(env, carol));
+        }
+
+        // TEST 6: Expired credential, depositAuth enabled but credential
+        // not authorized by bob.
+        // checkDepositPreauth in preclaim calls checkDepositPreauth which
+        // finds no match and returns tecNO_PERMISSION. doApply never runs, so
+        // the expired credential is not cleaned up by this transaction. This is
+        // a deliberate tradeoff: allowing doApply to run solely for cleanup
+        // would require bypassing the preclaim short-circuit, forcing every
+        // validator to run the expensive ZK proof verification before
+        // discovering the authorization failure. Expired credentials here will
+        // be cleaned up opportunistically by a future transaction that
+        // references them.
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+            env(fset(bob, asfDepositAuth));
+            env.close();
+
+            auto const credIdx = createExpiringCredential(env, carol);
+
+            // Advance ledger past credential expiration
+            env.close(std::chrono::seconds(expireTime));
+
+            // Fails with tecNO_PERMISSION.
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecNO_PERMISSION,
+            });
+            env.close();
+
+            // Expired credential is not deleted
+            BEAST_EXPECT(!credentialDeleted(env, carol));
+        }
+    }
+
+    void
+    testSendCredentialValidation(FeatureBitset features)
+    {
+        testcase("Send credential validation");
+        using namespace test::jtx;
+
+        // Tests for credentials::checkFields (preflight) and
+        // credentials::valid (preclaim) validation.
+        //
+        // Preflight checks (temMALFORMED):
+        //   - Empty credentials array
+        //   - Array size exceeds maxCredentialsArraySize (8)
+        //   - Duplicate credential IDs in array
+        //
+        // Preclaim checks (tecBAD_CREDENTIALS):
+        //   - Credential doesn't exist
+        //   - Credential doesn't belong to source account
+        //   - Credential not accepted (lsfAccepted flag not set)
+
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dpIssuer("dpIssuer");
+        char const credType[] = "KYC";
+
+        // TEST 1: Preflight - Empty Credentials Array
+        {
+            Env env(*this, features);
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = std::vector{},
+                .err = temMALFORMED,
+            });
+        }
+
+        // TEST 2: Preflight - Credentials Array Too Large
+        {
+            Env env(*this, features);
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            std::vector tooManyCredentials;
+            tooManyCredentials.reserve(9);
+            for (int i = 0; i < 9; ++i)
+                tooManyCredentials.push_back(to_string(uint256(i)));
+
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = tooManyCredentials,
+                .err = temMALFORMED,
+            });
+        }
+
+        // TEST 3: Preflight - Duplicate Credentials
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            env(credentials::create(carol, dpIssuer, credType));
+            env.close();
+            env(credentials::accept(carol, dpIssuer, credType));
+            env.close();
+
+            auto const jv = credentials::ledgerEntry(env, carol, dpIssuer, credType);
+            std::string const credIdx = jv[jss::result][jss::index].asString();
+
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx, credIdx}},
+                .err = temMALFORMED,
+            });
+        }
+
+        // TEST 4: Preclaim - Credential Doesn't Exist
+        {
+            Env env(*this, features);
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            std::string const fakeCredIdx = to_string(uint256(999));
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{fakeCredIdx}},
+                .err = tecBAD_CREDENTIALS,
+            });
+        }
+
+        // TEST 5: Preclaim - Credential Doesn't Belong to Source Account
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            // Create credential for BOB (not carol)
+            env(credentials::create(bob, dpIssuer, credType));
+            env.close();
+            env(credentials::accept(bob, dpIssuer, credType));
+            env.close();
+
+            auto const jv = credentials::ledgerEntry(env, bob, dpIssuer, credType);
+            std::string const credIdx = jv[jss::result][jss::index].asString();
+
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecBAD_CREDENTIALS,
+            });
+        }
+
+        // TEST 6: Preclaim - Credential Not Accepted
+        {
+            Env env(*this, features);
+            env.fund(XRP(50000), dpIssuer);
+            env.close();
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            // Create credential but DON'T accept it
+            env(credentials::create(carol, dpIssuer, credType));
+            env.close();
+
+            auto const jv = credentials::ledgerEntry(env, carol, dpIssuer, credType);
+            std::string const credIdx = jv[jss::result][jss::index].asString();
+
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{credIdx}},
+                .err = tecBAD_CREDENTIALS,
+            });
+        }
+
+        // TEST 7: Preflight - sfCredentialIDs requires featureCredentials.
+        // Even with featureConfidentialTransfer enabled, supplying
+        // CredentialIDs while featureCredentials is disabled must be
+        // rejected in preflight via checkExtraFeatures.
+        {
+            Env env(*this, features - featureCredentials);
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50},
+                 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
+            auto& mpt = confEnv.mpt;
+
+            auto constexpr kCredIdx =
+                "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288BE4";
+
+            mpt.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 10,
+                .credentials = {{kCredIdx}},
+                .err = temDISABLED,
+            });
+        }
+    }
+
+    // Bob creates the AMM, but Bob is not the MPT holder checked below.
+    // The AMM has its own pseudo-account (`ammHolder`) that can hold the
+    // public MPT pool balance. That pseudo-account cannot normally
+    // initialize confidential state because the confidential txn's must be
+    // signed by sfAccount, and the AMM pseudo-account has no signing key.
+    // So this is a construction/impossibility test: public AMM MPT state exists
+    // but the corresponding confidential AMM clawback flow is not normally reachable.
+    void
+    testAMMHolderCannotHaveConfidentialStateClawback(FeatureBitset features)
+    {
+        testcase("AMM holder cannot have confidential state");
+        using namespace test::jtx;
+
+        Account const alice("alice");
+        Account const bob("bob");
+
+        for (bool const enablePseudoAccount : {false, true})
+        {
+            Env env{
+                *this,
+                enablePseudoAccount ? features | featureSingleAssetVault
+                                    : features - featureSingleAssetVault};
+
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .flags = kMptDexFlags | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({.account = bob});
+            mptAlice.pay(alice, bob, 1'000);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            AMM const amm(env, bob, XRP(100), mptAlice(100));
+            Account const ammHolder("amm", amm.ammAccount());
+            auto const ammSle = env.le(keylet::account(ammHolder.id()));
+
+            BEAST_EXPECT(ammSle && ammSle->isFieldPresent(sfAMMID));
+            BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
+
+            BEAST_EXPECT(!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedInbox));
+            BEAST_EXPECT(
+                !mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedSpending));
+            BEAST_EXPECT(
+                !mptAlice.getEncryptedBalance(ammHolder, MPTTester::issuerEncryptedBalance));
+            BEAST_EXPECT(
+                !mptAlice.getEncryptedBalance(ammHolder, MPTTester::auditorEncryptedBalance));
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = ammHolder,
+                .amt = 100,
+                .proof = strHex(gMakeZeroBuffer(kEcClawbackProofLength)),
+                .err = tecNO_PERMISSION,
+            });
+        }
+    }
+
+    // Exercises every Confidential Transfer transaction type (MPTokenIssuanceSet,
+    // Convert, MergeInbox, Send, ConvertBack) using tickets instead of regular account
+    // sequence numbers.
+    void
+    testWithTickets(FeatureBitset features)
+    {
+        testcase("Confidential transfer with tickets");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.authorize({.account = carol});
+        mptAlice.pay(alice, bob, 100);
+        mptAlice.pay(alice, carol, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+
+        // MPTokenIssuanceSet with ticket, registers alice's issuer key.
+        {
+            std::uint32_t const ticketSeq = env.seq(alice) + 1;
+            env(ticket::create(alice, 1));
+            mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice), .ticketSeq = ticketSeq});
+        }
+
+        // ConfidentialMPTConvert with ticket, first convert registers bob's key.
+        {
+            std::uint32_t const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .ticketSeq = ticketSeq,
+            });
+            env.require(MptBalance(mptAlice, bob, 50));
+        }
+
+        // ConfidentialMPTConvert with ticket
+        {
+            std::uint32_t const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            mptAlice.convert({.account = bob, .amt = 20, .ticketSeq = ticketSeq});
+            env.require(MptBalance(mptAlice, bob, 30));
+        }
+
+        // ConfidentialMPTMergeInbox with ticket.
+        {
+            std::uint32_t const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq});
+        }
+
+        mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
+        mptAlice.mergeInbox({.account = carol});
+
+        // ConfidentialMPTSend with ticket.
+        {
+            std::uint32_t const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            mptAlice.send({.account = bob, .dest = carol, .amt = 10, .ticketSeq = ticketSeq});
+        }
+
+        // Merge carol's inbox so her spending balance includes the received send.
+        mptAlice.mergeInbox({.account = carol});
+
+        // ConfidentialMPTConvertBack with ticket.
+        // The convertBack proof context hash must use the ticket sequence.
+        {
+            std::uint32_t const ticketSeq = env.seq(carol) + 1;
+            env(ticket::create(carol, 1));
+            mptAlice.convertBack({.account = carol, .amt = 10, .ticketSeq = ticketSeq});
+            // carol converted 50, received 10 from bob, then converted back 10 → public 60
+            env.require(MptBalance(mptAlice, carol, 60));
+        }
+    }
+
+    // Verifies that cryptographic proofs in Convert transactions are bound to
+    // the ticket sequence rather than the account sequence.
+    // A proof built with the ticket sequence passes.
+    void
+    testConvertTicketProofBinding(FeatureBitset features)
+    {
+        testcase("Convert proof binds to ticket sequence");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .holderCount = 0,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+        mptAlice.generateKeyPair(bob);
+
+        uint64_t const amt = 30;
+        Buffer const bf = generateBlindingFactor();
+        Buffer const holderCt = mptAlice.encryptAmount(bob, amt, bf);
+        Buffer const issuerCt = mptAlice.encryptAmount(alice, amt, bf);
+
+        std::uint32_t const ticketSeq1 = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+
+        // Invalid: Schnorr proof built with the account seq (env.seq(bob)) rather
+        // than the ticket seq (ticketSeq1).
+        {
+            BEAST_EXPECT(env.seq(bob) != ticketSeq1);
+            uint256 const badCtxHash =
+                getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob));
+            auto const badProof = requireOptional(
+                mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof.");
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .proof = strHex(badProof),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .ticketSeq = ticketSeq1,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        std::uint32_t const ticketSeq2 = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+
+        // Valid: proof auto-generated by convert() using ticketSeq2; context hashes match.
+        mptAlice.convert({
+            .account = bob,
+            .amt = amt,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .holderEncryptedAmt = holderCt,
+            .issuerEncryptedAmt = issuerCt,
+            .blindingFactor = bf,
+            .ticketSeq = ticketSeq2,
+        });
+        env.require(MptBalance(mptAlice, bob, 70));
+    }
+
+    // Exercises ticket-specific error codes for confidential transfer transactions:
+    void
+    testDestinationTag(FeatureBitset features)
+    {
+        testcase("test Destination Tag");
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
+            tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+
+        // Set RequireDest on carol
+        env(fset(carol, asfRequireDest));
+        env.close();
+
+        // Send without destination tag — rejected
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+            .proof = getTrivialSendProofHex(),
+            .senderEncryptedAmt = getTrivialCiphertext(),
+            .destEncryptedAmt = getTrivialCiphertext(),
+            .issuerEncryptedAmt = getTrivialCiphertext(),
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecDST_TAG_NEEDED,
+        });
+
+        // Send with destination tag — succeeds (passes preclaim,
+        // reaches ZKP verification with the real proof)
+        mptAlice.send({.account = bob, .dest = carol, .amt = 10, .destinationTag = 42});
+
+        // Verify the destination tag is in the confirmed transaction
+        auto const tx = env.tx();
+        BEAST_EXPECT(tx);
+        BEAST_EXPECT(tx->isFieldPresent(sfDestinationTag));
+        BEAST_EXPECT((*tx)[sfDestinationTag] == 42);
+
+        env(fclear(carol, asfRequireDest));
+        env.close();
+
+        // Send without destination tag when not required — succeeds
+        mptAlice.mergeInbox({.account = carol});
+        mptAlice.send({.account = bob, .dest = carol, .amt = 10});
+    }
+
+    // terPRE_TICKET when the ticket doesn't exist yet, and tefNO_TICKET when
+    // the ticket has already been consumed or was never created.
+    void
+    testTicketErrors(FeatureBitset features)
+    {
+        testcase("Confidential transfer ticket errors");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .holderCount = 0,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+        mptAlice.generateKeyPair(bob);
+
+        // Give bob an inbox balance so MergeInbox has something to merge.
+        mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)});
+
+        // Use MergeInbox as the confidential transfer transaction under test
+        // so that ticket errors are isolated from cryptographic verification.
+
+        // terPRE_TICKET: ticket sequence is far in the future and hasn't been created.
+        mptAlice.mergeInbox(
+            {.account = bob, .ticketSeq = env.seq(bob) + 100, .err = terPRE_TICKET});
+
+        // Create one ticket and use it successfully.
+        std::uint32_t const ticketSeq = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+        mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq});
+
+        // tefNO_TICKET: attempt to reuse the same (already-consumed) ticket.
+        mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq, .err = tefNO_TICKET});
+
+        // tefNO_TICKET: ticket sequence is in the past but was never created.
+        mptAlice.mergeInbox({.account = bob, .ticketSeq = 1, .err = tefNO_TICKET});
+    }
+
+    // Bob sends 100 MPT to Carol. Carol Merge Inbox. Carol sends 50 MPT to Dave.
+    // Inner 3rd txn (Carol sends to Dave) fails because the proof is built with
+    // when Carols's spending balance is 0. (before she received funds from Bob)
+    //
+    // Also tests Bob sending to two recipients (Carol and Dave) in a single
+    // batch. Even though Bob has enough balance for both, the second send's
+    // balance-linkage proof becomes incorrect once inner 1 updates Bob's encrypted
+    // spending, so fails
+    void
+    testBatchConfidentialSend(FeatureBitset features)
+    {
+        testcase("Batch confidential send - merge inbox dependency");
+        using namespace test::jtx;
+
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            // bob = A (100 spending), carol = B (0), dave = C (0)
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            // Build the batch:
+            //   Batch Txn 1 bob -> carol 100 : valid proof, bob spending=100
+            //   Batch Txn 2 carol -> mergeInbox : valid JV
+            //   Batch Txn 3 carol->dave 50  : Invalid
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            // 3 signers, Bob, Carol, Dave
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 3);
+
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 100}, bobSeq + 1);
+            auto const jv2 = mpt.mergeInboxJV({.account = carol});
+            auto const jv3 = mpt.sendJV({.account = carol, .dest = dave, .amt = 50}, carolSeq + 1);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Inner(jv3, carolSeq + 1),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // AllOrNothing: inner 3 fails
+            // bob's spending must remain 100; carol's inbox must remain 0.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+        }
+
+        // Bob sends to two recipients (Carol and Dave) in one batch.
+        // Bob has 150, enough for both sends individually.  However, batch txn 1
+        // changes Bob's encrypted spending on the ledger; batch txn 2 was built
+        // against the old enc(150) so its balance-linkage proof is stale.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 150, 0);
+
+            // tfAllOrNothing — rejects the whole batch as 2nd txn proof is incorrect
+            {
+                auto const bobSeq = env.seq(bob);
+                auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+                auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
+                auto const jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
+
+                env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                    batch::Inner(jv1, bobSeq + 1),
+                    batch::Inner(jv2, bobSeq + 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                // Nothing applied: bob stays 150, carol and dave inbox stay 0.
+                BEAST_EXPECT(
+                    mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 150);
+                BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+                BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+            }
+
+            // If we change batch mode to be tfIndependent — txn 1 applies, inner 2 fails.
+            {
+                auto const bobSeq = env.seq(bob);
+                auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+                auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
+                auto const jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
+
+                env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
+                    batch::Inner(jv1, bobSeq + 1),
+                    batch::Inner(jv2, bobSeq + 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                // bob 150→100, carol inbox 0→50
+                BEAST_EXPECT(
+                    mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+                BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 50);
+                // dave gets nothing
+                BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+            }
+        }
+
+        // Now, Bob sends Confidential MPT to 2 accounts in one batch.
+        // However this time, the second txn proof is calculated using the
+        // correct encrypted(spending) proof, so it should pass.
+        {
+            // bob has exactly enough for both sends.
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 200, 0);
+
+            {
+                auto const bobSeq = env.seq(bob);
+                auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+                // jv1 is built against the current ledger state (spending=200).
+                auto const jv1 =
+                    mpt.sendJV({.account = bob, .dest = carol, .amt = 100}, bobSeq + 1);
+
+                // Compute post-jv1 state without touching the ledger.
+                auto const chain1 = mpt.chainAfterSend(bob, 100, jv1);
+
+                // jv2 proof is built against predicted spending=100, version=N+1.
+                auto const jv2 =
+                    mpt.sendJV({.account = bob, .dest = dave, .amt = 100}, bobSeq + 2, chain1);
+
+                env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                    batch::Inner(jv1, bobSeq + 1),
+                    batch::Inner(jv2, bobSeq + 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                // Both txns applied: bob 200→0, carol inbox=100, dave inbox=100.
+                BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 0);
+                BEAST_EXPECT(
+                    mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 100);
+                BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 100);
+            }
+
+            // Now Bob has 150, but tries to send two 100 in one batch.
+            // This fails because Bob doesn't have enough MPT balance.
+            {
+                Env env2{*this, features};
+                Account const alice2("alice");
+                Account const bob2("bob");
+                Account const carol2("carol");
+                Account const dave2("dave");
+
+                MPTTester mpt2(env2, alice2, {.holders = {bob2, carol2, dave2}});
+                setupBatchEnv(mpt2, alice2, bob2, carol2, dave2, 150, 0);
+
+                auto const bobSeq = env2.seq(bob2);
+                auto const batchFee = batch::calcConfidentialBatchFee(env2, 0, 2);
+
+                auto const jv1 =
+                    mpt2.sendJV({.account = bob2, .dest = carol2, .amt = 100}, bobSeq + 1);
+                auto const chain1 = mpt2.chainAfterSend(bob2, 100, jv1);
+
+                auto const jv2 =
+                    mpt2.sendJV({.account = bob2, .dest = dave2, .amt = 100}, bobSeq + 2, chain1);
+
+                env2(
+                    batch::outer(bob2, bobSeq, batchFee, tfAllOrNothing),
+                    batch::Inner(jv1, bobSeq + 1),
+                    batch::Inner(jv2, bobSeq + 2),
+                    Ter(tesSUCCESS));
+                env2.close();
+
+                // AllOrNothing: inner 2 fails → nothing applied.
+                BEAST_EXPECT(
+                    mpt2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 150);
+                BEAST_EXPECT(
+                    mpt2.getDecryptedBalance(carol2, MPTTester::holderEncryptedInbox) == 0);
+                BEAST_EXPECT(mpt2.getDecryptedBalance(dave2, MPTTester::holderEncryptedInbox) == 0);
+            }
+        }
+    }
+    void
+    testBatchConfidentialConvertAndConvertBack(FeatureBitset features)
+    {
+        testcase("Batch confidential convert and convertBack");
+        using namespace test::jtx;
+
+        // convert + convertBack in one AllOrNothing batch, both valid.
+        //
+        // Bob has regular=50, spending=100.
+        // jv1: convert 50 regular → inbox  (Schnorr proof; does NOT touch spending/version)
+        // jv2: convertBack 30 spending → regular  (proof against spending=100, version=V)
+        //
+        // Since jv1 leaves spending and version unchanged, jv2's proof is still
+        // valid when it executes, so both inner txns succeed.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            // bob: spending=100, regular=0 after setupBatchEnv;
+            // pay 50 more to give bob regular MPT to convert in the batch.
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+            mpt.pay(alice, bob, 50);
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // jv1: convert 50 regular MPT into confidential inbox
+            auto const jv1 = mpt.convertJV({.account = bob, .amt = 50}, bobSeq + 1);
+            // jv2: convert 30 spending back to regular MPT
+            auto const jv2 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq + 2);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, bobSeq + 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            //   regular (mptAmount): 50 (pre) - 50 (convert) + 30 (convertBack) = 30
+            //   spending balance: 100 - 30 = 70
+            //   inbox:    0   + 50 (from convert) = 50
+            env.require(MptBalance(mpt, bob, 30));
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 70);
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 50);
+        }
+
+        // convert + mergeInbox + convertBack, stale convertBack proof.
+        //
+        // jv1: convert 50 regular → inbox
+        // jv2: mergeInbox (inbox 50 → spending, version V → V+1)
+        // jv3: convertBack 30 (proof built against spending=100, version=V)
+        //
+        // After jv2 applies, spending=150 and version=V+1, so jv3's
+        // proof is stale.  AllOrNothing rejects the whole batch.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+            mpt.pay(alice, bob, 50);
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 3);
+
+            auto const jv1 = mpt.convertJV({.account = bob, .amt = 50}, bobSeq + 1);
+            auto const jv2 = mpt.mergeInboxJV({.account = bob});
+            // jv3 proof is built against spending=100, version=V (pre-batch)
+            auto const jv3 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq + 3);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, bobSeq + 2),
+                batch::Inner(jv3, bobSeq + 3),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // jv3 fails so nothing is applied.
+            env.require(MptBalance(mpt, bob, 50));
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0);
+        }
+    }
+
+    // Tests a batch containing all four confidential MPT operations, Send,
+    // Convert, ConvertBack, and MergeInbox in a single AllOrNothing batch.
+    void
+    testBatchConfidentialMixTransactions(FeatureBitset features)
+    {
+        testcase("Batch confidential mixed operations");
+        using namespace test::jtx;
+
+        // send(bob→carol) + convert(carol) + convertBack(dave)
+        //  + mergeInbox(carol) in one AllOrNothing batch.
+        //
+        // Setup:
+        //   bob:   spending=100, regular=0
+        //   carol: spending=0,   regular=50
+        //   dave:  spending=50,  regular=0
+        //
+        // After the batch:
+        //   bob   spending: 100 -> 70  (sent 30 to carol)
+        //   carol inbox:    0+30(send)+50(convert)=80 -> merged -> spending=80, inbox=0
+        //   dave  spending: 50 -> 30; regular: 0 -> 20
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            // bob: spending=100. carol: key registered, spending=0.
+            // dave: key registered, spending=0 initially.
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+            // Give carol 50 regular MPT to convert in the batch.
+            mpt.pay(alice, carol, 50);
+            // Give dave 50 regular MPT then convert to confidential spending.
+            mpt.pay(alice, dave, 50);
+            mpt.convert({.account = dave, .amt = 50});
+            mpt.mergeInbox({.account = dave});
+
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const daveSeq = env.seq(dave);
+            // 2 extra signers (carol, dave), 4 inner txns
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 4);
+
+            // jv1: bob sends 30 to carol
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1);
+            // jv2: carol converts her 50 regular MPT to confidential
+            auto const jv2 = mpt.convertJV({.account = carol, .amt = 50}, carolSeq);
+            // jv3: dave converts 20 spending back to regular MPT
+            auto const jv3 = mpt.convertBackJV({.account = dave, .amt = 20}, daveSeq);
+            // jv4: carol merges inbox into spending
+            //   (inbox = 30 from jv1 + 50 from jv2 = 80 at execution time)
+            auto const jv4 = mpt.mergeInboxJV({.account = carol});
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Inner(jv3, daveSeq),
+                batch::Inner(jv4, carolSeq + 1),
+                batch::Sig(carol, dave),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // All four applied:
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 70);
+            // carol's inbox was merged: spending=80, inbox=0
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 80);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+            // dave: spending=30, regular=20
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 30);
+            env.require(MptBalance(mpt, dave, 20));
+        }
+
+        // bob send + bob convertBack in one AllOrNothing batch.
+        //
+        // The Send applies first and increments Bob's version counter.
+        // The ConvertBack proof was built against the pre-Send (spending=100,
+        // version=V), so batch txn is rejected.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // jv1: bob sends 30 to carol (spending 100->70, version V->V+1)
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1);
+            // jv2: bob convertBack 40 , proof built against spending=100, version=V
+            auto const jv2 = mpt.convertBackJV({.account = bob, .amt = 40}, bobSeq + 2);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, bobSeq + 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // AllOrNothing: jv2 fails (stale proof) → nothing applied.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+        }
+    }
+
+    // Verifies that batch transactions work correctly when tickets are used instead
+    // of sequence numbers
+    void
+    testBatchAllOrNothing(FeatureBitset features)
+    {
+        testcase("Batch confidential MPT - all or nothing");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+
+        MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+        // bob=100 spending, carol=60 spending, dave=0
+        setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+        // bob sends dave 10, carol sends dave 5, independent, both valid.
+        {
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1);
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Both txn applied: bob's balance 100→90, carol 60→55, dave inbox 0→15
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15);
+        }
+    }
+
+    void
+    testBatchOnlyOne(FeatureBitset features)
+    {
+        testcase("Batch confidential MPT - only one");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+
+        MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+        // bob=100 spending, carol=60 spending, dave=0
+        setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+        // bob sends dave 200 (invalid), carol sends dave 300 (invalid)
+        {
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            // Both proofs fail range check (amount > balance)
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1);
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 300}, carolSeq);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfOnlyOne),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // No success found → nothing applied; balances unchanged
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+        }
+
+        // bob sends dave 200 (invalid), carol sends dave 5 (valid)
+        {
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            auto jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1);
+            auto jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfOnlyOne),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Only carol's send applied: carol 60→55, dave inbox 0→5, bob unchanged
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 5);
+        }
+    }
+
+    void
+    testBatchUntilFailure(FeatureBitset features)
+    {
+        testcase("Batch confidential MPT - until failure");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+
+        MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+        // bob=100 spending, carol=60 spending, dave=0
+        setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+        // first fails → none applied
+        // Bob sends Dave 200 (invalid — stops immediately)
+        {
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1);
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfUntilFailure),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
+        }
+
+        // Bob sends dave 10, Carol sends dave 5 — both valid and independent
+        {
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1);
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfUntilFailure),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Both applied: bob 100→90, carol 60→55, dave inbox 0→15
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15);
+        }
+    }
+
+    void
+    testBatchIndependent(FeatureBitset features)
+    {
+        testcase("Batch confidential MPT - independent");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+
+        MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+        // bob=100 spending, carol=60 spending, dave=0
+        setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+        // Bob sends dave 10 (valid), Carol sends dave 300
+        // (invalid), Carol sends Dave 5 (valid). Carol's
+        // balance is still 60 because the preceding send failed).
+        {
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 3);
+
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1);
+
+            // Carol trying to send dave 300 but own balance only 60
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 300}, carolSeq);
+            auto const jv3 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq + 1);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Inner(jv3, carolSeq + 1),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // inner 1 (bob→dave 10) applied: bob 100→90
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90);
+            // inner 2 failed (carol not changed), inner 3 applied: carol 60→55
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
+            // dave inbox: 10 (from bob) + 5 (from carol inner 3) = 15
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15);
+        }
+    }
+
+    // Tests batching ConfidentialMPTConvert and a ConfidentialMPTConvertBack
+    // in the same batch transaction. Because Convert only modifies the inbox
+    // (never the spending balance or the version counter), a ConvertBack proof
+    // built against the pre-batch spending balance is still valid when both
+    // appear in the same batch.
+    void
+    testBatchWithTickets(FeatureBitset features)
+    {
+        testcase("Batch confidential MPT with tickets");
+        using namespace test::jtx;
+
+        // outer batch uses a ticket.
+        // The inner send proofs are still bound to regular account sequences.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            // Bob creates one ticket to use for the outer batch.
+            std::uint32_t const outerTicketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            // 0 extra signers: all inner txns are from bob;
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // When the outer uses a ticket (seq=0), inner txns start from bobSeq, bobSeq+1.
+            // jv2 must use chain state predicted after jv1 since both sends are from bob.
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
+            auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
+            auto const jv2 =
+                mpt.sendJV({.account = bob, .dest = dave, .amt = 20}, bobSeq + 1, chain1);
+
+            env(batch::outer(bob, 0, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq),
+                batch::Inner(jv2, bobSeq + 1),
+                ticket::Use(outerTicketSeq),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Both sends applied: bob 100→40, carol inbox=40, dave inbox=20.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 40);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 20);
+        }
+
+        // inner transactions each consume their own ticket.
+        // The send proof context hash must be bound to the ticket sequence, not the
+        // account sequence. sendJV receives the ticket seq as its `seq` parameter.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            // Bob creates two tickets for the two inner sends.
+            std::uint32_t const ticketSeq1 = env.seq(bob) + 1;
+            std::uint32_t const ticketSeq2 = env.seq(bob) + 2;
+            env(ticket::create(bob, 2));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // jv1: proof bound to ticketSeq1.
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, ticketSeq1);
+            // jv2: proof bound to ticketSeq2, spending state predicted after jv1.
+            auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
+            auto const jv2 =
+                mpt.sendJV({.account = bob, .dest = dave, .amt = 30}, ticketSeq2, chain1);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, 0, ticketSeq1),
+                batch::Inner(jv2, 0, ticketSeq2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Both sends applied: bob 100→30, carol inbox=40, dave inbox=30.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30);
+        }
+
+        // inner send uses wrong sequence (account seq instead of ticket seq)
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            std::uint32_t const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // Proof intentionally built with account seq (bobSeq+1) instead of ticketSeq.
+            auto const badJV = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1);
+            auto const jv2 = mpt.mergeInboxJV({.account = bob});
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(badJV, 0, ticketSeq),
+                batch::Inner(jv2, bobSeq + 1),
+                Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+        }
+    }
+
+    // Basic tests of confidential transfer through delegation. Verifies that a delegated account
+    // with the appropriate permissions can execute confidential transfer transactions
+    // on behalf of the delegator.
+    void
+    testConfidentialDelegation(FeatureBitset features)
+    {
+        testcase("Confidential transfers through delegation");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const carol{"carol"};
+        Account const dave{"dave"};
+
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+        env.fund(XRP(10000), dave);
+        env.close();
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
+                tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.authorize({.account = carol});
+        mptAlice.pay(alice, bob, 200);
+        mptAlice.pay(alice, carol, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
+
+        // Bob delegates Convert, MergeInbox to dave.
+        env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"}));
+        env.close();
+
+        // Carol has no permission from bob to convert on his behalf.
+        mptAlice.convert({
+            .account = bob,
+            .amt = 10,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .delegate = carol,
+            .err = terNO_DELEGATE_PERMISSION,
+        });
+
+        // Dave executes Convert on behalf of bob, registering bob's key.
+        mptAlice.convert({
+            .account = bob,
+            .amt = 100,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .delegate = dave,
+        });
+        env.require(MptBalance(mptAlice, bob, 100));
+
+        // Dave executes Convert again on behalf of bob (no key registration).
+        mptAlice.convert({.account = bob, .amt = 50, .delegate = dave});
+
+        // Dave executes MergeInbox on behalf of bob.
+        mptAlice.mergeInbox({.account = bob, .delegate = dave});
+
+        // Carol converts and merge inbox.
+        mptAlice.convert({
+            .account = carol,
+            .amt = 100,
+            .holderPubKey = mptAlice.getPubKey(carol),
+        });
+        mptAlice.mergeInbox({.account = carol});
+
+        // Dave does not have permission to send on behalf of bob.
+        mptAlice.send(
+            {.account = bob,
+             .dest = carol,
+             .amt = 10,
+             .delegate = dave,
+             .err = terNO_DELEGATE_PERMISSION});
+
+        // Bob delegates ConfidentialMPTSend to dave.
+        env(delegate::set(
+            bob,
+            dave,
+            {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox", "ConfidentialMPTSend"}));
+        env.close();
+
+        // Dave executes Send on behalf of bob.
+        mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave});
+        mptAlice.mergeInbox({.account = carol});
+
+        // Dave does not have permission to convert back on behalf of bob.
+        mptAlice.convertBack(
+            {.account = bob, .amt = 10, .delegate = dave, .err = terNO_DELEGATE_PERMISSION});
+
+        // Bob delegates ConfidentialMPTConvertBack to dave.
+        env(delegate::set(
+            bob,
+            dave,
+            {"ConfidentialMPTConvert",
+             "ConfidentialMPTMergeInbox",
+             "ConfidentialMPTSend",
+             "ConfidentialMPTConvertBack"}));
+        env.close();
+
+        // Dave executes ConvertBack on behalf of bob.
+        mptAlice.convertBack({.account = bob, .amt = 10, .delegate = dave});
+
+        // Dave does not have permission to clawback on behalf of alice.
+        mptAlice.confidentialClaw(
+            {.holder = bob, .amt = 130, .delegate = dave, .err = terNO_DELEGATE_PERMISSION});
+
+        // Alice delegates ConfidentialMPTClawback to dave.
+        env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
+        env.close();
+
+        // Dave executes Clawback on behalf of alice.
+        mptAlice.confidentialClaw({.holder = bob, .amt = 130, .delegate = dave});
+    }
+
+    // Verifies that revoking delegation prevents further delegated operations.
+    void
+    testDelegationRevocation(FeatureBitset features)
+    {
+        testcase("Confidential delegation revocation");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const carol{"carol"};
+
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+        env.fund(XRP(10000), carol);
+        env.close();
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
+
+        // Creating the Delegate SLE consumes one owner reserve slot for bob.
+        auto const bobOwnersBefore = ownerCount(env, bob);
+        env(delegate::set(bob, carol, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"}));
+        env.close();
+        env.require(Owners(bob, bobOwnersBefore + 1));
+
+        // Carol converts and merge inbox on behalf of bob.
+        mptAlice.convert({
+            .account = bob,
+            .amt = 50,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .delegate = carol,
+        });
+        mptAlice.mergeInbox({.account = bob, .delegate = carol});
+
+        // Bob revokes all permissions, deletes the Delegate SLE, releasing the reserve.
+        env(delegate::set(bob, carol, std::vector{}));
+        env.close();
+        env.require(Owners(bob, bobOwnersBefore));
+
+        // Carol can no longer convert on behalf of bob.
+        mptAlice.convert({
+            .account = bob,
+            .amt = 30,
+            .delegate = carol,
+            .err = terNO_DELEGATE_PERMISSION,
+        });
+
+        // Bob can still convert by himself.
+        mptAlice.convert({.account = bob, .amt = 30});
+    }
+
+    // Verifies that a delegated confidential transfer works correctly when an
+    // auditor is configured on the issuance.
+    void
+    testDelegationWithAuditor(FeatureBitset features)
+    {
+        testcase("Confidential delegation with auditor");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const carol{"carol"};
+        Account const dave{"dave"};
+        Account const auditor{"auditor"};
+
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
+        env.fund(XRP(10000), dave);
+        env.close();
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.authorize({.account = carol});
+        mptAlice.pay(alice, bob, 100);
+        mptAlice.pay(alice, carol, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.generateKeyPair(auditor);
+        mptAlice.set({
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+        });
+
+        // Bob delegates Convert and Send permissions to dave.
+        env(delegate::set(bob, dave, {"ConfidentialMPTSend", "ConfidentialMPTConvert"}));
+        env.close();
+
+        // Dave converts on behalf of bob.
+        mptAlice.convert({
+            .account = bob,
+            .amt = 50,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .delegate = dave,
+        });
+        mptAlice.mergeInbox({.account = bob});
+
+        mptAlice.convert({
+            .account = carol,
+            .amt = 50,
+            .holderPubKey = mptAlice.getPubKey(carol),
+        });
+        mptAlice.mergeInbox({.account = carol});
+
+        // Dave sends on behalf of bob.
+        mptAlice.send({.account = bob, .dest = carol, .amt = 20, .delegate = dave});
+        mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave});
+
+        // Bob delegates ConvertBack and Send permissions to auditor.
+        env(delegate::set(bob, auditor, {"ConfidentialMPTSend", "ConfidentialMPTConvertBack"}));
+        env.close();
+
+        // auditor can send and convert back on behalf of bob as well.
+        mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = auditor});
+        mptAlice.convertBack({.account = bob, .amt = 10, .delegate = auditor});
+    }
+
+    // Verifies that a non-issuer delegating clawback to a third party does not
+    // allow that party to execute clawback, since clawback is issuer-only.
+    void
+    testDelegationClawbackIssuerOnly(FeatureBitset features)
+    {
+        testcase("Confidential clawback delegation requires issuer");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const carol{"carol"};
+        Account const dave{"dave"};
+
+        ConfidentialEnv const confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 50},
+             {.account = carol, .payAmount = 100, .convertAmount = 100}},
+            tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+        env.fund(XRP(10000), dave);
+        env.close();
+
+        // Bob delegates Clawback permission to dave.
+        env(delegate::set(bob, dave, {"ConfidentialMPTClawback"}));
+        env.close();
+
+        // Dave attempts clawback on behalf of bob targetting bob, but since bob is not the issuer,
+        // the transaction should be rejected.
+        {
+            json::Value jv;
+            jv[jss::Account] = bob.human();
+            jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
+            jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+            jv[sfHolder] = bob.human();
+            jv[sfMPTAmount.jsonName] = "50";
+            jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0');
+            env(jv, delegate::As(dave), Ter(temMALFORMED));
+        }
+
+        // Dave attempts clawback on behalf of bob targeting carol, but since bob is not the issuer,
+        // the transaction should be rejected.
+        {
+            json::Value jv;
+            jv[jss::Account] = bob.human();
+            jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
+            jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+            jv[sfHolder] = carol.human();
+            jv[sfMPTAmount.jsonName] = "100";
+            jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0');
+            env(jv, delegate::As(dave), Ter(temMALFORMED));
+        }
+    }
+
+    // Batch with delegated ConfidentialMPTSend txs, covering stale and updated inner
+    // send proofs.
+    void
+    testBatchDelegatedSend(FeatureBitset features)
+    {
+        testcase("Batch ConfidentialMPTSend with delegation");
+        using namespace test::jtx;
+
+        // AllOrNothing: two delegated sends from bob via dave, second proof is
+        // stale once the first send updates bob's spending, whole batch rolls back.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // jv1: proof against spending balance 100
+            auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 60}, bobSeq + 1);
+            jv1[jss::Delegate] = dave.human();
+            // jv2: proof also against spending balance 100, which is stale once jv1 applies
+            auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
+            jv2[jss::Delegate] = dave.human();
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, bobSeq + 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Stale proof on jv2, AllOrNothing rolls back everything.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+        }
+
+        // AllOrNothing: two delegated sends with correctly chained proofs both apply.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            // jv1: proof against spending balance 100.
+            auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1);
+            jv1[jss::Delegate] = dave.human();
+            auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
+            // jv2: proof against predicted spending balance 60.
+            auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 40}, bobSeq + 2, chain1);
+            jv2[jss::Delegate] = dave.human();
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, bobSeq + 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Both inner tx applied
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 20);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 40);
+        }
+    }
+
+    // Test missing delegation permission inside a batch.
+    void
+    testBatchDelegationMissingPermission(FeatureBitset features)
+    {
+        testcase("Batch delegation missing permission");
+        using namespace test::jtx;
+
+        // AllOrNothing: dave has no Send permission from bob, so the delegated
+        // inner send fails. The whole batch rolls back.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+            // Bob grants dave only MergeInbox, not Send.
+            env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox"}));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            // jv1: direct send from carol (valid proof).
+            auto const jv1 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
+            // jv2: delegated send, fails because dave has no Send permission.
+            auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
+            jv2[jss::Delegate] = dave.human();
+
+            env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, carolSeq),
+                batch::Inner(jv2, bobSeq + 1),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // jv1 applied in the batch view, then jv2 failed, so
+            // AllOrNothing discards both inner effects.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+        }
+
+        // Independent: the delegated confidential send is skipped because lack of permission. The
+        // send from carol still applies.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+            // Bob does not grant dave any permissions.
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
+            jv1[jss::Delegate] = dave.human();
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
+
+            env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
+                batch::Inner(jv1, bobSeq + 1),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // jv1 failed and jv2 applied.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 30);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30);
+        }
+    }
+
+    // Test batch outer signer is the delegated account.
+    void
+    testBatchDelegatedSendWithDelegateAsOuterAccount(FeatureBitset features)
+    {
+        testcase("Test batch delegated send with delegate as outer account");
+        using namespace test::jtx;
+
+        // Dave has delegation permission, but the inner Account is bob.
+        // Without bob's BatchSigner, the batch is rejected.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
+            env.close();
+
+            auto const daveSeq = env.seq(dave);
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+
+            auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
+            jv1[jss::Delegate] = dave.human();
+            auto const jv2 = mpt.mergeInboxJV({.account = dave});
+
+            env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq),
+                batch::Inner(jv2, daveSeq + 1),
+                Ter(temBAD_SIGNER));
+            env.close();
+
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+        }
+
+        // Dave submits a mixed batch: bob signs inner tx1, and
+        // dave is the Delegate account signing for inner tx2.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
+
+            env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
+            env.close();
+
+            auto const daveSeq = env.seq(dave);
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+
+            auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
+            auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
+            auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1, chain1);
+            jv2[jss::Delegate] = dave.human();
+
+            // Dave is outer; bob signs because his account appears in inner txns.
+            env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq),
+                batch::Inner(jv2, bobSeq + 1),
+                batch::Sig(bob),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Both sends applied: bob 100→30, carol inbox=70.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 70);
+        }
+
+        // Verify the delegator Bob's BatchSigner does not bypass the missing delegation permission.
+        // The delegated inner send fails.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+
+            MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
+            setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+
+            // Bob does not grant dave any permissions.
+            auto const daveSeq = env.seq(dave);
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
+
+            auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq);
+            jv1[jss::Delegate] = dave.human();
+            auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
+
+            env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
+                batch::Inner(jv1, bobSeq),
+                batch::Inner(jv2, carolSeq),
+                batch::Sig(bob, carol),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // jv1 fails before jv2 is attempted.
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
+            BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+        }
+    }
+
+    // Mixed batch with delegated and non-delegated inner confidential MPT transactions.
+    void
+    testBatchDelegatedConfidentialMix(FeatureBitset features)
+    {
+        testcase("Batch delegated confidential multiple operations");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+        Account const erin("erin");
+        Account const frank("frank");
+
+        MPTTester mpt(env, alice, {.holders = {bob, carol, dave, frank}});
+        setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
+        mpt.pay(alice, bob, 50);
+        env.fund(XRP(10000), erin);
+        env.close();
+
+        mpt.authorize({.account = frank});
+        mpt.pay(alice, frank, 40);
+        mpt.generateKeyPair(frank);
+
+        env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTConvertBack"}));
+        env(delegate::set(carol, erin, {"ConfidentialMPTSend"}));
+        env(delegate::set(bob, erin, {"ConfidentialMPTMergeInbox"}));
+        env.close();
+
+        auto const daveSeq = env.seq(dave);
+        auto const bobSeq = env.seq(bob);
+        auto const carolSeq = env.seq(carol);
+        auto const frankSeq = env.seq(frank);
+        auto const batchFee = batch::calcConfidentialBatchFee(env, 3, 6);
+
+        // Dave submits the batch. Bob's convert and convertback use Dave as Delegate;
+        // Carol's send and Bob's mergeInbox use Erin as Delegate. Frank's
+        // convert and mergeInbox are non-delegated.
+        auto jv1 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq);
+        jv1[jss::Delegate] = dave.human();
+        auto jv2 = mpt.convertJV({.account = bob, .amt = 20}, bobSeq + 1);
+        jv2[jss::Delegate] = dave.human();
+        auto jv3 = mpt.sendJV({.account = carol, .dest = bob, .amt = 15}, carolSeq);
+        jv3[jss::Delegate] = erin.human();
+        auto const jv4 = mpt.convertJV(
+            {.account = frank, .amt = 25, .holderPubKey = mpt.getPubKey(frank)}, frankSeq);
+        auto const jv5 = mpt.mergeInboxJV({.account = frank});
+        auto jv6 = mpt.mergeInboxJV({.account = bob});
+        jv6[jss::Delegate] = erin.human();
+
+        env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
+            batch::Inner(jv1, bobSeq),
+            batch::Inner(jv2, bobSeq + 1),
+            batch::Inner(jv3, carolSeq),
+            batch::Inner(jv4, frankSeq),
+            batch::Inner(jv5, frankSeq + 1),
+            batch::Inner(jv6, bobSeq + 2),
+            batch::Sig(bob, carol, frank),
+            Ter(tesSUCCESS));
+        env.close();
+
+        env.require(MptBalance(mpt, bob, 60));
+        BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 105);
+        BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0);
+        env.require(MptBalance(mpt, carol, 0));
+        BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 45);
+        BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+        env.require(MptBalance(mpt, frank, 15));
+        BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedSpending) == 25);
+        BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedInbox) == 0);
+        env.require(MptBalance(mpt, dave, 0));
+        BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 0);
+        BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
+        auto const outstandingBalance = mpt.getIssuanceOutstandingBalance();
+        BEAST_EXPECT(outstandingBalance && *outstandingBalance == 250);
+        BEAST_EXPECT(mpt.getIssuanceConfidentialBalance() == 175);
+    }
+
+    // Test invalid scenarios for delegation with tickets.
+    void
+    testInvalidDelegationWithTickets(FeatureBitset features)
+    {
+        testcase("Invalid cases for delegation with tickets");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+        env.fund(XRP(10000), carol);
+        env.close();
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.pay(alice, bob, 200);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
+
+        // Bob grants carol permissions.
+        env(delegate::set(bob, carol, {"ConfidentialMPTConvert"}));
+        env.close();
+
+        uint64_t const amt = 10;
+        auto const bf = generateBlindingFactor();
+        auto const holderCt = mptAlice.encryptAmount(bob, amt, bf);
+        auto const issuerCt = mptAlice.encryptAmount(alice, amt, bf);
+
+        // Invalid: proof built with wrong ticket sequence (ticketSeq + 1).
+        {
+            auto const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+
+            auto const badCtxHash =
+                getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq + 1);
+            auto const badProof = requireOptional(
+                mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof.");
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .proof = strHex(badProof),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .delegate = carol,
+                .ticketSeq = ticketSeq,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Invalid: proof built with account sequence instead of ticket sequence.
+        {
+            auto const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+            auto const badCtxHash = getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob));
+            auto const badProof = requireOptional(
+                mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof.");
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .proof = strHex(badProof),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .delegate = carol,
+                .ticketSeq = ticketSeq,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Invalid: ticket sequence is far in the future and hasn't been created yet.
+        {
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .delegate = carol,
+                .ticketSeq = env.seq(bob) + 100,
+                .err = terPRE_TICKET,
+            });
+        }
+
+        // Invalid: ticket sequence is in the past but was never created.
+        {
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .delegate = carol,
+                .ticketSeq = 1,
+                .err = tefNO_TICKET,
+            });
+        }
+
+        // Invalid: the delegated account, carol, creates a ticket and uses it.
+        {
+            auto const carolTicketSeq = env.seq(carol) + 1;
+            env(ticket::create(carol, 1));
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .delegate = carol,
+                .ticketSeq = carolTicketSeq,
+                .err = tefNO_TICKET,
+            });
+        }
+
+        // Invalid: proof bound to a ticket sequence but submitted without a ticket,
+        // using account sequence.
+        {
+            auto const ticketSeq = env.seq(bob) + 1;
+            env(ticket::create(bob, 1));
+
+            // Build proof using ticketSeq.
+            auto const ctxHashForTicket =
+                getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq);
+            auto const proof = requireOptional(
+                mptAlice.getSchnorrProof(bob, ctxHashForTicket), "Missing Schnorr Proof.");
+
+            // Submit without ticket.
+            mptAlice.convert({
+                .account = bob,
+                .amt = amt,
+                .proof = strHex(proof),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCt,
+                .issuerEncryptedAmt = issuerCt,
+                .blindingFactor = bf,
+                .delegate = carol,
+                .err = tecBAD_PROOF,
+            });
+        }
+    }
+
+    // Verifies that delegation works correctly when the delegating account uses
+    // tickets instead of regular sequence numbers. The proof must bind to the
+    // ticket sequence, not the account sequence.
+    void
+    testDelegationWithTickets(FeatureBitset features)
+    {
+        testcase("Confidential delegation with tickets");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+        env.fund(XRP(10000), dave);
+        env.close();
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.authorize({.account = carol});
+        mptAlice.pay(alice, bob, 200);
+        mptAlice.pay(alice, carol, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
+
+        // Bob grants dave permissions.
+        env(delegate::set(
+            bob,
+            dave,
+            {"ConfidentialMPTConvert",
+             "ConfidentialMPTMergeInbox",
+             "ConfidentialMPTSend",
+             "ConfidentialMPTConvertBack"}));
+        // Alice grants dave permission to clawback on her behalf.
+        env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
+        env.close();
+
+        // Dave executes Convert on behalf of bob using ticket.
+        auto ticketSeq = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+        BEAST_EXPECT(env.seq(bob) != ticketSeq);
+        mptAlice.convert({
+            .account = bob,
+            .amt = 100,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .delegate = dave,
+            .ticketSeq = ticketSeq,
+        });
+        env.require(MptBalance(mptAlice, bob, 100));
+
+        // MergeInbox using ticket with delegation.
+        ticketSeq = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+        BEAST_EXPECT(env.seq(bob) != ticketSeq);
+        mptAlice.mergeInbox({.account = bob, .delegate = dave, .ticketSeq = ticketSeq});
+
+        // Carol converts and merges inbox to receive from bob.
+        mptAlice.convert({
+            .account = carol,
+            .amt = 50,
+            .holderPubKey = mptAlice.getPubKey(carol),
+        });
+        mptAlice.mergeInbox({.account = carol});
+
+        // Send using ticket with delegation.
+        ticketSeq = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+        BEAST_EXPECT(env.seq(bob) != ticketSeq);
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 20,
+            .delegate = dave,
+            .ticketSeq = ticketSeq,
+        });
+
+        // ConvertBack using ticket with delegation.
+        ticketSeq = env.seq(bob) + 1;
+        env(ticket::create(bob, 1));
+        BEAST_EXPECT(env.seq(bob) != ticketSeq);
+        mptAlice.convertBack({
+            .account = bob,
+            .amt = 10,
+            .delegate = dave,
+            .ticketSeq = ticketSeq,
+        });
+
+        // Clawback using ticket with delegation.
+        ticketSeq = env.seq(alice) + 1;
+        env(ticket::create(alice, 1));
+        BEAST_EXPECT(env.seq(alice) != ticketSeq);
+        mptAlice.confidentialClaw({
+            .holder = bob,
+            .amt = 70,
+            .delegate = dave,
+            .ticketSeq = ticketSeq,
+        });
+    }
+
+    void
+    testWithFeats(FeatureBitset features)
+    {
+        // DepositAuth, credentials, and destination tag interactions.
+        testSendDepositPreauth(features);
+        testSendCredentialValidation(features);
+        testDestinationTag(features);
+
+        // AMM/pseudo-account interaction.
+        testAMMHolderCannotHaveConfidentialStateClawback(features);
+
+        // Ticket interactions.
+        testWithTickets(features);
+        testConvertTicketProofBinding(features);
+        testTicketErrors(features);
+
+        // Batch interactions.
+        testBatchConfidentialSend(features);
+        testBatchConfidentialConvertAndConvertBack(features);
+        testBatchConfidentialMixTransactions(features);
+        testBatchAllOrNothing(features);
+        testBatchOnlyOne(features);
+        testBatchUntilFailure(features);
+        testBatchIndependent(features);
+        testBatchWithTickets(features);
+
+        // Permission delegation interactions.
+        testConfidentialDelegation(features);
+        testDelegationRevocation(features);
+        testDelegationWithAuditor(features);
+        testDelegationClawbackIssuerOnly(features);
+        testBatchDelegatedSend(features);
+        testBatchDelegationMissingPermission(features);
+        testBatchDelegatedSendWithDelegateAsOuterAccount(features);
+        testBatchDelegatedConfidentialMix(features);
+        testInvalidDelegationWithTickets(features);
+        testDelegationWithTickets(features);
+    }
+
+public:
+    void
+    run() override
+    {
+        using namespace test::jtx;
+        FeatureBitset const all{testableAmendments()};
+
+        testWithFeats(all);
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(ConfidentialTransferExtended, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp
new file mode 100644
index 0000000000..e6faf0a2ae
--- /dev/null
+++ b/src/test/app/ConfidentialTransfer_test.cpp
@@ -0,0 +1,8208 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class ConfidentialTransfer_test : public ConfidentialTransferTestBase
+{
+    void
+    testConvert(FeatureBitset features)
+    {
+        testcase("Convert");
+        using namespace test::jtx;
+
+        // Basic convert test
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 20,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 40,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 40,
+            });
+        }
+
+        // Edge case: minimum amount (1)
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 1);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 1,
+            });
+        }
+
+        // Edge case: kMaxMpTokenAmount
+        // Using raw JSON to avoid automatic decryption checks in MPTTester
+        // which don't work for very large amounts (brute-force decryption is slow)
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, kMaxMpTokenAmount);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            // First convert with amt=0 to register public key (uses MPTTester)
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            // Second convert with kMaxMpTokenAmount using raw JSON
+            Buffer const blindingFactor = generateBlindingFactor();
+            auto const holderCiphertext =
+                mptAlice.encryptAmount(bob, kMaxMpTokenAmount, blindingFactor);
+            auto const issuerCiphertext =
+                mptAlice.encryptAmount(alice, kMaxMpTokenAmount, blindingFactor);
+
+            json::Value jv;
+            jv[jss::Account] = bob.human();
+            jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
+            jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+            jv[sfMPTAmount.jsonName] = std::to_string(kMaxMpTokenAmount);
+            jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext);
+            jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext);
+            jv[sfBlindingFactor.jsonName] = strHex(blindingFactor);
+
+            env(jv, Ter(tesSUCCESS));
+
+            // Verify the public balance was reduced
+            env.require(MptBalance(mptAlice, bob, 0));
+        }
+    }
+
+    void
+    testConvertWithAuditor(FeatureBitset features)
+    {
+        testcase("Convert with auditor");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const auditor("auditor");
+        MPTTester mptAlice(
+            env,
+            alice,
+            {
+                .holders = {bob},
+                .auditor = auditor,
+            });
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.authorize({
+            .account = bob,
+        });
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(auditor);
+
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+        });
+
+        mptAlice.generateKeyPair(bob);
+
+        mptAlice.convert({
+            .account = bob,
+            .amt = 0,
+            .holderPubKey = mptAlice.getPubKey(bob),
+        });
+
+        mptAlice.convert({
+            .account = bob,
+            .amt = 20,
+        });
+
+        mptAlice.convert({
+            .account = bob,
+            .amt = 30,
+        });
+    }
+
+    void
+    testConvertPreflight(FeatureBitset features)
+    {
+        testcase("Convert preflight");
+        using namespace test::jtx;
+
+        // Alice (issuer) tries to convert her own tokens - should fail
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice);
+
+            mptAlice.create({
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.convert({
+                .account = alice,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(alice),
+                .err = temMALFORMED,
+            });
+        }
+
+        {
+            Env env{*this, features - featureConfidentialTransfer};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .err = temDISABLED,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = temDISABLED,
+            });
+        }
+
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = alice,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = temMALFORMED,
+            });
+
+            // Holder encrypted amount is empty (length 0)
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = Buffer{},
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // Issuer encrypted amount is empty (length 0)
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .issuerEncryptedAmt = Buffer{},
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // Auditor encrypted amount has invalid length (must be 66 bytes)
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .auditorEncryptedAmt = gMakeZeroBuffer(10),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // Auditor encrypted amount has correct length but invalid data
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .auditorEncryptedAmt = getBadCiphertext(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // Amount exceeds maximum allowed MPT amount
+            mptAlice.convert({
+                .account = bob,
+                .amt = kMaxMpTokenAmount + 1,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = temBAD_AMOUNT,
+            });
+
+            // Holder encrypted amount has correct length but invalid data
+            mptAlice.convert({
+                .account = bob,
+                .amt = 1,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = getBadCiphertext(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // Issuer encrypted amount has correct length but invalid data (not
+            // a valid EC point)
+            mptAlice.convert({
+                .account = bob,
+                .amt = 1,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .issuerEncryptedAmt = getBadCiphertext(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // Holder public key is invalid (empty buffer)
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = Buffer{},
+                .err = temMALFORMED,
+            });
+
+            // Holder public key has correct length but invalid EC point data
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
+                .err = temMALFORMED,
+            });
+        }
+
+        // when registering holder pub key, the transaction must include a
+        // Schnorr proof of knowledge for the corresponding secret key
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .fillSchnorrProof = false,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = temMALFORMED,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .fillSchnorrProof = false,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = temMALFORMED,
+            });
+
+            // proof length is invalid
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .proof = std::string(10, 'A'),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = temMALFORMED,
+            });
+        }
+
+        // when holder pub key already registered, Schnorr proof must not be
+        // provided
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // this will register bob's pub key,
+            // and convert 10 to confidential balance
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            // proof must not be provided after pub key was registered
+            mptAlice.convert({
+                .account = bob,
+                .amt = 20,
+                .fillSchnorrProof = true,
+                .err = temMALFORMED,
+            });
+        }
+    }
+
+    void
+    testConvertInvalidProofContextBinding(FeatureBitset features)
+    {
+        testcase("Convert proof context binding");
+        using namespace test::jtx;
+
+        auto runBadProof = [&](auto makeContextHash) {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({.account = bob});
+            mptAlice.authorize({.account = carol});
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            auto const proof =
+                mptAlice.getSchnorrProof(bob, makeContextHash(env, mptAlice, alice, bob, carol));
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .proof = strHex(requireOptional(proof, "Missing proof")),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecBAD_PROOF,
+            });
+        };
+
+        // Wrong account in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const& carol) {
+            return getConvertContextHash(carol.id(), mpt.issuanceID(), env.seq(bob));
+        });
+
+        // Wrong issuance ID in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const&,
+                        Account const& alice,
+                        Account const& bob,
+                        Account const&) {
+            return getConvertContextHash(
+                bob.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(bob));
+        });
+
+        // Wrong transaction sequence in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const&) {
+            return getConvertContextHash(bob.id(), mpt.issuanceID(), env.seq(bob) + 1);
+        });
+    }
+
+    void
+    testSet(FeatureBitset features)
+    {
+        testcase("Set");
+        using namespace test::jtx;
+
+        // Set keys on issuance that already has confidential amounts enabled
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+            });
+        }
+
+        // Enable confidential amounts flag only (no keys)
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+            });
+        }
+
+        // Set keys when enabling confidential amounts in the same tx
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+            });
+
+            // Verify lsfMPTCanHoldConfidentialBalance flag is set
+            BEAST_EXPECT(mptAlice.checkFlags(
+                lsfMPTCanTransfer | lsfMPTCanLock | lsfMPTCanHoldConfidentialBalance));
+
+            // Verify keys are persisted on the issuance
+            auto const sle = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
+            BEAST_EXPECT(sle);
+            BEAST_EXPECT(sle->isFieldPresent(sfIssuerEncryptionKey));
+            BEAST_EXPECT(sle->isFieldPresent(sfAuditorEncryptionKey));
+        }
+    }
+
+    void
+    testSetPreflight(FeatureBitset features)
+    {
+        testcase("Set preflight");
+        using namespace test::jtx;
+
+        {
+            Env env{*this, features - featureConfidentialTransfer};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .err = temDISABLED,
+            });
+        }
+
+        // pub key is invalid
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            // Issuer pub key is invalid (empty)
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = Buffer{},
+                .err = temMALFORMED,
+            });
+
+            // Issuer pub key has correct length but invalid EC point data
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = gMakeZeroBuffer(kEcPubKeyLength),
+                .err = temMALFORMED,
+            });
+
+            // Auditor key is invalid length
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = gMakeZeroBuffer(10),
+                .err = temMALFORMED,
+            });
+
+            // Auditor key has correct length but invalid EC point data
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = gMakeZeroBuffer(kEcPubKeyLength),
+                .err = temMALFORMED,
+            });
+
+            // Cannot set auditor key without issuer key
+            mptAlice.set({
+                .account = alice,
+                .auditorPubKey = mptAlice.getPubKey(alice),
+                .err = temMALFORMED,
+            });
+
+            // Cannot set Holder and issuer Keys in the same transaction
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .err = temMALFORMED,
+            });
+
+            // Cannot set Holder and auditor Keys in the same transaction
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .auditorPubKey = mptAlice.getPubKey(alice),
+                .err = temMALFORMED,
+            });
+        }
+    }
+
+    void
+    testSetPreclaim(FeatureBitset features)
+    {
+        testcase("Set preclaim");
+        using namespace test::jtx;
+
+        // Cannot set issuer key if confidential amounts not enabled
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Cannot update issuer public key once set
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            // First set issuer key - should succeed
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+            });
+
+            // Try to update issuer key - should fail
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(bob),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Cannot update issuer and auditor public keys once set
+        // Note: trying to set only auditor key fails in preflight (temMALFORMED)
+        // so we must provide both keys, which fails on issuer key check first
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(auditor);
+
+            // Set issuer and auditor keys - should succeed
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+            });
+
+            // Try to update both keys - fails on issuer key check first
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(bob),
+                .auditorPubKey = mptAlice.getPubKey(alice),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Cannot set auditor key if confidential amounts not enabled
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Cannot set keys when mutation of canConfidentialAmount is disallowed
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            // Create with tmfMPTCannotEnableCanHoldConfidentialBalance
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+                .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+
+            // Trying to enable confidential amounts and set keys fails
+            // because the issuance cannot mutate canConfidentialAmount
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Set issuer key first, then auditor key in a separate tx
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(auditor);
+
+            // Set issuer key only
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+            });
+
+            // Set auditor key in a separate tx - requires issuer key in tx
+            // (preflight enforces auditor key requires issuer key)
+            // This fails because issuer key is already set on ledger
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+                .err = tecNO_PERMISSION,
+            });
+        }
+    }
+
+    void
+    testTransferFee(FeatureBitset features)
+    {
+        testcase("test transfer fee");
+        using namespace test::jtx;
+
+        // MPTokenIssuanceCreate: cannot create with both TransferFee > 0 and
+        // tfMPTCanHoldConfidentialBalance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            mptAlice.create({
+                .transferFee = 100,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+                .err = temBAD_TRANSFER_FEE,
+            });
+
+            // transferFee being 0 is allowed, even with tfMPTCanHoldConfidentialBalance
+            mptAlice.create({
+                .transferFee = 0,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+        }
+
+        // MPTokenIssuanceSet (preflight): cannot enable confidential amounts and
+        // set TransferFee > 0 in the same transaction
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+                .mutableFlags = tmfMPTCanMutateTransferFee,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+                .transferFee = 100,
+                .err = temBAD_TRANSFER_FEE,
+            });
+        }
+
+        // MPTokenIssuanceSet (preclaim): cannot enable confidential amounts on
+        // an issuance that already has a non-zero TransferFee
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            mptAlice.create({
+                .transferFee = 100,
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+                .mutableFlags = tmfMPTCanMutateTransferFee,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // MPTokenIssuanceSet (preclaim): cannot set TransferFee > 0 on an
+        // issuance that already has lsfMPTCanHoldConfidentialBalance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            MPTTester mptAlice(env, alice, {.holders = {}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+                .mutableFlags = tmfMPTCanMutateTransferFee,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .transferFee = 100,
+                .err = tecNO_PERMISSION,
+            });
+
+            // Setting transfer fee to 0 is allowed, but have no effect.
+            mptAlice.set({
+                .account = alice,
+                .transferFee = 0,
+            });
+        }
+    }
+
+    void
+    testConvertPreclaim(FeatureBitset features)
+    {
+        testcase("Convert preclaim");
+        using namespace test::jtx;
+
+        // tfMPTCanHoldConfidentialBalance is not set on issuance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // issuer has not uploaded their sfIssuerEncryptionKey
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // issuance does not exist
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.destroy();
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // bob has not created MPToken
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // Verification of Issuer and and holder ciphertexts
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = getTrivialCiphertext(),
+                .err = tecBAD_PROOF,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .err = tecBAD_PROOF,
+            });
+
+            std::uint64_t const amount = 10;
+            Buffer const blindingFactor = generateBlindingFactor();
+            Buffer const holderCiphertext = mptAlice.encryptAmount(bob, amount, blindingFactor);
+
+            // Holder ciphertext is valid for the amount and
+            // blinding factor, but the issuer ciphertext is encrypted under a
+            // different public key than the registered issuer key.
+            Buffer const wrongIssuerCiphertext =
+                mptAlice.encryptAmount(carol, amount, blindingFactor);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = amount,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .holderEncryptedAmt = holderCiphertext,
+                .issuerEncryptedAmt = wrongIssuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // trying to convert more than what bob has
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 200,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecINSUFFICIENT_FUNDS,
+            });
+        }
+
+        // holder cannot upload pk again
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)});
+
+            // cannot upload pk again
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecDUPLICATE,
+            });
+        }
+
+        // cannot convert if locked
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTLock,
+            });
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecLOCKED,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnlock,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+        }
+
+        // cannot convert if unauth
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
+                    tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            // Unauthorize bob
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnauthorize,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecNO_AUTH,
+            });
+
+            // auth bob
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+        }
+
+        // frozen account cannot bypass freeze check with amount=0
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // lock bob
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTLock,
+            });
+
+            mptAlice.generateKeyPair(bob);
+
+            // amount=0 should still be rejected when locked
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecLOCKED,
+            });
+        }
+
+        // unauthorized account cannot bypass auth check with amount=0
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
+                    tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            // Unauthorize bob
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnauthorize,
+            });
+
+            // amount=0 should still be rejected when unauthorized
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecNO_AUTH,
+            });
+        }
+
+        // cannot convert if auditor key is set, but auditor amount is not
+        // provided
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const auditor("auditor");
+            MPTTester mptAlice(
+                env,
+                alice,
+                {
+                    .holders = {bob},
+                    .auditor = auditor,
+                });
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set(
+                {.account = alice,
+                 .issuerPubKey = mptAlice.getPubKey(alice),
+                 .auditorPubKey = mptAlice.getPubKey(auditor)});
+
+            // no auditor encrypted amt provided
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .fillAuditorEncryptedAmt = false,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // cannot convert if tx include auditor ciphertext, but does not have
+        // auditing enabled
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            // there is no auditor key set
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .auditorEncryptedAmt = getTrivialCiphertext(),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Auditor key set successfully, auditor ciphertext mathematically
+        // correct, but contains invalid data (mismatching amount).
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const auditor("auditor");
+            MPTTester mptAlice(
+                env,
+                alice,
+                {
+                    .holders = {bob},
+                    .auditor = auditor,
+                });
+
+            mptAlice.create({
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set(
+                {.account = alice,
+                 .issuerPubKey = mptAlice.getPubKey(alice),
+                 .auditorPubKey = mptAlice.getPubKey(auditor)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .auditorEncryptedAmt = getTrivialCiphertext(),
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // invalid proof when registering holder pub key
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .proof = std::string(kEcSchnorrProofLength * 2, 'A'),
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // no holder key on ledger and no key in tx
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // bob has not registered a holder key, and doesn't provide one
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // all public balance already converted, try to convert more
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            // convert entire public balance
+            mptAlice.convert({
+                .account = bob,
+                .amt = 100,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            env.require(MptBalance(mptAlice, bob, 0));
+
+            // try to convert 1 more — no public balance left
+            mptAlice.convert({
+                .account = bob,
+                .amt = 1,
+                .err = tecINSUFFICIENT_FUNDS,
+            });
+        }
+    }
+
+    void
+    testMergeInbox(FeatureBitset features)
+    {
+        testcase("Merge inbox");
+        using namespace test::jtx;
+
+        // Merge with an empty inbox should succeed as a no-op.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({.account = bob});
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.mergeInbox({.account = bob});
+            // Inbox is empty after the first merge; the second merge is a no-op.
+            mptAlice.mergeInbox({.account = bob});
+        }
+
+        // Makes sure if merge inbox version is UINT32_MAX, the next merge wraps
+        // the version back to 0.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({.account = bob});
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+            });
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            // Force the on-ledger version to UINT32_MAX, then apply a merge and
+            // confirm the version wraps around to 0.
+            auto const wrappedFrom = std::numeric_limits::max();
+            auto const jt = env.jt(mptAlice.mergeInboxJV({.account = bob}));
+            BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+                auto const sle = std::const_pointer_cast(
+                    view.read(keylet::mptoken(mptAlice.issuanceID(), bob.id())));
+                if (!sle)
+                    return false;
+
+                (*sle)[sfConfidentialBalanceVersion] = wrappedFrom;
+                view.rawReplace(sle);
+
+                auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, env.journal);
+                BEAST_EXPECT(result.ter == tesSUCCESS);
+                return result.applied;
+            }));
+
+            BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) == 0);
+        }
+    }
+
+    void
+    testMergeInboxPreflight(FeatureBitset features)
+    {
+        testcase("Merge inbox preflight");
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.authorize({
+            .account = bob,
+        });
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+
+        mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+        mptAlice.generateKeyPair(bob);
+
+        mptAlice.convert({
+            .account = bob,
+            .amt = 40,
+            .holderPubKey = mptAlice.getPubKey(bob),
+        });
+
+        mptAlice.mergeInbox({
+            .account = alice,
+            .err = temMALFORMED,
+        });
+
+        env.disableFeature(featureConfidentialTransfer);
+        env.close();
+
+        mptAlice.mergeInbox({
+            .account = bob,
+            .err = temDISABLED,
+        });
+    }
+
+    void
+    testMergeInboxPreclaim(FeatureBitset features)
+    {
+        testcase("Merge inbox preclaim");
+        using namespace test::jtx;
+
+        // issuance does not exist
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.destroy();
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.mergeInbox({
+                .account = bob,
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // tfMPTCanHoldConfidentialBalance is not set on issuance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.mergeInbox({
+                .account = bob,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // no mptoken
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.mergeInbox({
+                .account = bob,
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // bob doesn't have encrypted balances
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.mergeInbox({
+                .account = bob,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // holder is locked
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            // lock bob
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTLock,
+            });
+
+            mptAlice.mergeInbox({
+                .account = bob,
+                .err = tecLOCKED,
+            });
+
+            // unlock bob
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnlock,
+            });
+
+            // should succeed now
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+        }
+
+        // holder not authorized
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance |
+                    tfMPTRequireAuth,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            // unauthorize bob
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnauthorize,
+            });
+
+            mptAlice.mergeInbox({
+                .account = bob,
+                .err = tecNO_AUTH,
+            });
+
+            // authorize bob again
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+
+            // should succeed now
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+        }
+    }
+
+    void
+    testSend(FeatureBitset features)
+    {
+        testcase("test confidential send");
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 60},
+             {.account = carol, .payAmount = 50, .convertAmount = 20}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // bob sends 10 to carol
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+        });
+
+        // bob sends 1 to carol again
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 1,
+        });
+
+        mptAlice.mergeInbox({
+            .account = carol,
+        });
+
+        // carol sends 15 back to bob
+        mptAlice.send({
+            .account = carol,
+            .dest = bob,
+            .amt = 15,
+        });
+    }
+
+    void
+    testSendWithAuditor(FeatureBitset features)
+    {
+        testcase("test confidential send with auditor");
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const auditor("auditor");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 60},
+             {.account = carol, .payAmount = 50, .convertAmount = 20}},
+            tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            auditor};
+        auto& mptAlice = confEnv.mpt;
+
+        // bob sends 10 to carol
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+        });
+
+        // bob sends 1 to carol again
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 1,
+        });
+
+        mptAlice.mergeInbox({
+            .account = carol,
+        });
+
+        // carol sends 15 back to bob
+        mptAlice.send({
+            .account = carol,
+            .dest = bob,
+            .amt = 15,
+        });
+    }
+
+    void
+    testSendPreflight(FeatureBitset features)
+    {
+        testcase("test ConfidentialMPTSend Preflight");
+        using namespace test::jtx;
+
+        // test disabled
+        {
+            Env env{*this, features - featureConfidentialTransfer};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create();
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .senderEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
+                .destEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
+                .issuerEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
+                .err = temDISABLED,
+            });
+        }
+
+        // test malformed
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+
+            // issuer can not be the same as sender
+            mptAlice.send({
+                .account = alice,
+                .dest = carol,
+                .amt = 10,
+                .err = temMALFORMED,
+            });
+
+            // can not send to self
+            mptAlice.send({
+                .account = bob,
+                .dest = bob,
+                .amt = 10,
+                .err = temMALFORMED,
+            });
+
+            // can not send to issuer
+            mptAlice.send({
+                .account = bob,
+                .dest = alice,
+                .amt = 10,
+                .err = temMALFORMED,
+            });
+
+            // sender encrypted amount wrong length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .senderEncryptedAmt = gMakeZeroBuffer(10),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // dest encrypted amount wrong length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .destEncryptedAmt = gMakeZeroBuffer(10),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // issuer encrypted amount wrong length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .issuerEncryptedAmt = gMakeZeroBuffer(10),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // sender encrypted amount malformed
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // dest encrypted amount malformed
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .destEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // issuer encrypted amount malformed
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .issuerEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // invalid proof length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = std::string(10, 'A'),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temMALFORMED,
+            });
+
+            // invalid amount Pedersen commitment length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .amountCommitment = gMakeZeroBuffer(100),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temMALFORMED,
+            });
+
+            // invalid balance Pedersen commitment length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = gMakeZeroBuffer(100),
+                .err = temMALFORMED,
+            });
+
+            // amount Pedersen commitment has correct length but invalid EC point data
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .amountCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temMALFORMED,
+            });
+
+            // balance Pedersen commitment has correct length but invalid EC point data
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength),
+                .err = temMALFORMED,
+            });
+        }
+
+        // test bad ciphertext
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const auditor("auditor");
+            MPTTester mptAlice(
+                env,
+                alice,
+                {
+                    .holders = {bob, carol},
+                    .auditor = auditor,
+                });
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set(
+                {.account = alice,
+                 .issuerPubKey = mptAlice.getPubKey(alice),
+                 .auditorPubKey = mptAlice.getPubKey(auditor)});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+
+            // auditor encrypted amount wrong length
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .auditorEncryptedAmt = gMakeZeroBuffer(10),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // auditor encrypted amount (correct length, invalid data)
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .auditorEncryptedAmt = getBadCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+        }
+    }
+
+    void
+    testSendPreclaim(FeatureBitset features)
+    {
+        testcase("test ConfidentialMPTSend Preclaim");
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+        Account const eve("eve");
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave, eve}});
+
+        // authorize bob, carol, dave (not eve)
+        mptAlice.create({
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
+                tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({
+            .account = bob,
+        });
+        mptAlice.authorize({
+            .account = alice,
+            .holder = bob,
+        });
+        mptAlice.authorize({
+            .account = carol,
+        });
+        mptAlice.authorize({
+            .account = alice,
+            .holder = carol,
+        });
+        mptAlice.authorize({
+            .account = dave,
+        });
+        mptAlice.authorize({
+            .account = alice,
+            .holder = dave,
+        });
+
+        // fund bob, carol (not dave or eve)
+        mptAlice.pay(alice, bob, 100);
+        mptAlice.pay(alice, carol, 50);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.generateKeyPair(dave);
+        mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+        // bob and carol convert some funds to confidential
+        mptAlice.convert({
+            .account = bob,
+            .amt = 60,
+            .holderPubKey = mptAlice.getPubKey(bob),
+            .err = tesSUCCESS,
+        });
+        mptAlice.convert({
+            .account = carol,
+            .amt = 20,
+            .holderPubKey = mptAlice.getPubKey(carol),
+            .err = tesSUCCESS,
+        });
+
+        // bob and carol merge inbox
+        mptAlice.mergeInbox({
+            .account = bob,
+        });
+        mptAlice.mergeInbox({
+            .account = carol,
+        });
+
+        // issuance not found
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // destroy the issuance
+            mptAlice.destroy();
+
+            json::Value jv;
+            jv[jss::Account] = bob.human();
+            jv[jss::Destination] = carol.human();
+            jv[jss::TransactionType] = jss::ConfidentialMPTSend;
+            jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+            jv[sfSenderEncryptedAmount] = strHex(getTrivialCiphertext());
+            jv[sfDestinationEncryptedAmount] = strHex(getTrivialCiphertext());
+            jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext());
+            jv[sfAmountCommitment] = strHex(getTrivialCommitment());
+            jv[sfBalanceCommitment] = strHex(getTrivialCommitment());
+            jv[sfZKProof] = getTrivialSendProofHex();
+
+            env(jv, Ter(tecOBJECT_NOT_FOUND));
+        }
+
+        // destination does not exist
+        {
+            Account const unknown("unknown");
+            mptAlice.send({
+                .account = bob,
+                .dest = unknown,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = getTrivialCiphertext(),
+                .destEncryptedAmt = getTrivialCiphertext(),
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = tecNO_TARGET,
+            });
+        }
+
+        // destination requires destination tag but none provided
+        {
+            env(fset(carol, asfRequireDest));
+            env.close();
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = getTrivialCiphertext(),
+                .destEncryptedAmt = getTrivialCiphertext(),
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = tecDST_TAG_NEEDED,
+            });
+
+            env(fclear(carol, asfRequireDest));
+            env.close();
+        }
+
+        // dave exists, but has no confidential fields (never converted)
+        {
+            mptAlice.send({
+                .account = bob,
+                .dest = dave,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = getTrivialCiphertext(),
+                .destEncryptedAmt = getTrivialCiphertext(),
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = tecNO_PERMISSION,
+            });
+            mptAlice.send({
+                .account = dave,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = getTrivialCiphertext(),
+                .destEncryptedAmt = getTrivialCiphertext(),
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // destination exists but has no MPT object.
+        {
+            mptAlice.send({
+                .account = bob,
+                .dest = eve,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = getTrivialCiphertext(),
+                .destEncryptedAmt = getTrivialCiphertext(),
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // issuance is locked globally
+        {
+            // lock issuance
+            mptAlice.set({
+                .account = alice,
+                .flags = tfMPTLock,
+            });
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .err = tecLOCKED,
+            });
+            // unlock issuance
+            mptAlice.set({
+                .account = alice,
+                .flags = tfMPTUnlock,
+            });
+            // now can send
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 1,
+            });
+        }
+
+        // sender is locked
+        {
+            // lock bob
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTLock,
+            });
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .err = tecLOCKED,
+            });
+            // unlock bob
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnlock,
+            });
+            // now can send
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 2,
+            });
+        }
+
+        // destination is locked
+        {
+            // lock carol
+            mptAlice.set({
+                .account = alice,
+                .holder = carol,
+                .flags = tfMPTLock,
+            });
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .err = tecLOCKED,
+            });
+            // unlock carol
+            mptAlice.set({
+                .account = alice,
+                .holder = carol,
+                .flags = tfMPTUnlock,
+            });
+            // now can send
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 3,
+            });
+        }
+
+        // sender not authorized
+        {
+            // unauthorize bob
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnauthorize,
+            });
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .err = tecNO_AUTH,
+            });
+            // authorize bob again
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+            // now can send
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 4,
+            });
+        }
+
+        // destination not authorized
+        {
+            // unauthorize carol
+            mptAlice.authorize({
+                .account = alice,
+                .holder = carol,
+                .flags = tfMPTUnauthorize,
+            });
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .err = tecNO_AUTH,
+            });
+            // authorize carol again
+            mptAlice.authorize({
+                .account = alice,
+                .holder = carol,
+            });
+            // now can send
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 5,
+            });
+        }
+
+        // cannot send when MPTCanTransfer is not set
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60},
+                 {.account = carol, .payAmount = 50, .convertAmount = 20}},
+                tfMPTCanLock | tfMPTCanHoldConfidentialBalance};
+            auto& mptAlice = confEnv.mpt;
+
+            // bob sends 10 to carol
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,  // will be encrypted internally
+                .err = tecNO_AUTH,
+            });
+        }
+
+        // Confidential MPTs should not have a transfer fee. Force malformed
+        // ledger state to cover the defensive preclaim check.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60},
+                 {.account = carol, .payAmount = 50, .convertAmount = 20}}};
+            auto& mptAlice = confEnv.mpt;
+
+            BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+                auto const issuance = std::const_pointer_cast(
+                    view.read(keylet::mptokenIssuance(mptAlice.issuanceID())));
+                if (!issuance)
+                    return false;
+
+                issuance->setFieldU16(sfTransferFee, 1);
+                view.rawReplace(issuance);
+                return true;
+            }));
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // bad proof
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60},
+                 {.account = carol, .payAmount = 50, .convertAmount = 20}}};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // No Auditor key set, but auditor encrypted amt provided
+        {
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .auditorEncryptedAmt = getTrivialCiphertext(),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Auditor CipherText is Valid, but does not match the Txn Amount
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const auditor("auditor");
+            MPTTester mptAlice(
+                env,
+                alice,
+                {
+                    .holders = {bob, carol},
+                    .auditor = auditor,
+                });
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.generateKeyPair(auditor);
+
+            mptAlice.set(
+                {.account = alice,
+                 .issuerPubKey = mptAlice.getPubKey(alice),
+                 .auditorPubKey = mptAlice.getPubKey(auditor)});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .auditorEncryptedAmt = getTrivialCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = tecBAD_PROOF,
+            });
+        }
+    }
+
+    void
+    testSendRangeProof(FeatureBitset features)
+    {
+        testcase("test ConfidentialMPTSend Range Proof");
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 1000, .convertAmount = 60},
+             {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        {
+            // Bob has 60, tries to send 70. Invalid remaining balance.
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 70,
+                .err = tecBAD_PROOF,
+            });
+
+            // Bob has 60, tries to send 61. Invalid remaining balance.
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 61,
+                .err = tecBAD_PROOF,
+            });
+
+            // Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance.
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 60,
+                .err = tesSUCCESS,
+            });
+        }
+
+        {
+            // Bob converts 100.
+            mptAlice.convert({
+                .account = bob,
+                .amt = 100,
+            });
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+
+            // Bob has 100, tries to send 2^64-1. Invalid remaining balance.
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = std::numeric_limits::max(),
+                .err = tecBAD_PROOF,
+            });
+
+            // Bob sends 1, remaining 99.
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 1,
+                .err = tesSUCCESS,
+            });
+
+            // Bob sends 100, but only has 99. Invalid remaining balance.
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 100,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // send when spending balance is 0 (key registered, inbox merged, but nothing converted)
+        {
+            // Register keys only (amt=0) for both parties — spending stays 0.
+            Env env2{*this, features};
+            Account const alice2("alice"), bob2("bob"), carol2("carol");
+            ConfidentialEnv zeroEnv{
+                env2,
+                alice2,
+                {{.account = bob2, .payAmount = 100, .convertAmount = 0},
+                 {.account = carol2, .payAmount = 50, .convertAmount = 0}}};
+            auto& mptAlice2 = zeroEnv.mpt;
+
+            // Trying to send any amount with 0 spending balance must fail:
+            // the range proof for < 0 is invalid.
+            mptAlice2.send({
+                .account = bob2,
+                .dest = carol2,
+                .amt = 1,
+                .err = tecBAD_PROOF,
+            });
+
+            BEAST_EXPECT(
+                mptAlice2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 0);
+        }
+
+        // todo: test m exceeding range, require using scala and refactor
+    }
+
+    /* The equality proof library and range proof library do not
+     * support generating proofs for amt=0 (they require a positive witness).
+     * To test the VERIFIER without crashing the helper, we bypass normal proof
+     * generation by supplying explicit ciphertexts, commitments, and a dummy
+     * (all-zero) proof.  The preflight has no temBAD_AMOUNT guard for
+     * ConfidentialMPTSend, so all validation occurs in verifySendProofs.
+     */
+    void
+    testSendZeroAmount(FeatureBitset features)
+    {
+        testcase("Send: zero amount — equality and range proof verifier behavior");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({.account = bob});
+        mptAlice.authorize({.account = carol});
+        mptAlice.pay(alice, bob, 100);
+        mptAlice.pay(alice, carol, 50);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+
+        mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+        mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)});
+        mptAlice.mergeInbox({.account = bob});
+
+        mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
+        mptAlice.mergeInbox({.account = carol});
+
+        Buffer const bf = generateBlindingFactor();
+
+        // equality proof verification for amt=0.
+        // Encrypt 0 under each participant's key.  The amount commitment is
+        // getTrivialCommitment() — a valid EC point that passes preflight's
+        // isValidCompressedECPoint check but is not the true PC for amt=0.
+        // The dummy ZKProof's equality component must be rejected by
+        // verifyMultiCiphertextEqualityProof.
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 0,
+            .proof = getTrivialSendProofHex(),
+            .senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf),
+            .destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf),
+            .issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf),
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecBAD_PROOF,
+        });
+
+        // range proof verification for amt=0.
+        // Identical construction; focuses on the bulletproof range check
+        // embedded in ZKProof.  The range proof for amount=0 with a dummy
+        // (all-zero) proof must also be rejected.
+        Buffer const bf2 = generateBlindingFactor();
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 0,
+            .proof = getTrivialSendProofHex(),
+            .senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf2),
+            .destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf2),
+            .issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf2),
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecBAD_PROOF,
+        });
+
+        // All rejected sends must leave balances unchanged.
+        BEAST_EXPECT(mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
+        BEAST_EXPECT(mptAlice.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+    }
+
+    void
+    testDelete(FeatureBitset features)
+    {
+        testcase("Delete");
+        using namespace test::jtx;
+
+        // cannot delete mptoken where it has encrypted balance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 100,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+                .flags = tfMPTUnauthorize,
+                .err = tecHAS_OBLIGATIONS,
+            });
+        }
+
+        // cannot delete mptoken where it has encrypted balance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 100,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+
+            // carol cannot delete even if he has encrypted zero amount
+            mptAlice.authorize({
+                .account = carol,
+                .flags = tfMPTUnauthorize,
+                .err = tecHAS_OBLIGATIONS,
+            });
+        }
+
+        // can delete mptoken if outstanding confidential balance is zero
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+                .flags = tfMPTUnauthorize,
+            });
+        }
+
+        // can delete mptoken if issuance has been destroyed and has
+        // encrypted zero balance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.destroy();
+
+            mptAlice.authorize({
+                .account = bob,
+                .flags = tfMPTUnauthorize,
+            });
+        }
+        // test with convert back and delete
+        // can delete mptoken if converted back (COA returns to zero)
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 100}}};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 100,
+            });
+
+            mptAlice.pay(bob, alice, 100);
+
+            // Should be able to delete as Confidential Outstanding amount is 0
+            mptAlice.authorize({
+                .account = bob,
+                .flags = tfMPTUnauthorize,
+            });
+        }
+
+        // removeEmptyHolding: vault share MPToken with confidential balance
+        // fields should not be deleted on VaultWithdraw
+        {
+            Env env{*this, features | featureSingleAssetVault};
+            Account const issuer("issuer");
+            Account const owner("owner");
+            Account const depositor("depositor");
+
+            MPTTester mptt{env, issuer, {.holders = {owner, depositor}}};
+            mptt.create({
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback,
+            });
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            env(pay(issuer, depositor, asset(1000)));
+            env.close();
+
+            test::jtx::Vault const vault{env};
+            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            // Get the share MPTID from vault
+            auto const vaultSle = env.le(vaultKeylet);
+            BEAST_EXPECT(vaultSle != nullptr);
+            auto const share = vaultSle->at(sfShareMPTID);
+
+            // Depositor deposits into vault
+            tx = vault.deposit(
+                {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)});
+            env(tx);
+            env.close();
+
+            // Verify depositor has share tokens
+            auto shareMpt = env.le(keylet::mptoken(share, depositor.id()));
+            BEAST_EXPECT(shareMpt != nullptr);
+
+            // Inject confidential balance fields on the share MPToken
+            // to simulate a scenario where vault shares somehow have
+            // confidential balances
+            env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+                // Set lsfMPTCanHoldConfidentialBalance on the share issuance
+                // so the invariant allows encrypted fields on the MPToken
+                auto issuance =
+                    std::const_pointer_cast(view.read(keylet::mptokenIssuance(share)));
+                if (!issuance)
+                    return false;
+                issuance->setFlag(lsfMPTCanHoldConfidentialBalance);
+                view.rawReplace(issuance);
+
+                auto const k = keylet::mptoken(share, depositor.id());
+                auto const sle = std::const_pointer_cast(view.read(k));
+                if (!sle)
+                    return false;
+                // Inject dummy confidential balance fields
+                Buffer dummyCiphertext(kEcGamalEncryptedTotalLength);
+                std::memset(dummyCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
+                dummyCiphertext.data()[0] = kEcCompressedPrefixEvenY;
+                dummyCiphertext.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
+                dummyCiphertext.data()[kEcCiphertextComponentLength - 1] = 0x01;
+                dummyCiphertext.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
+                sle->setFieldVL(sfConfidentialBalanceSpending, dummyCiphertext);
+                sle->setFieldVL(sfConfidentialBalanceInbox, dummyCiphertext);
+                sle->setFieldVL(sfIssuerEncryptedBalance, dummyCiphertext);
+                view.rawReplace(sle);
+                return true;
+            });
+
+            // Withdraw everything - which should fail because of the confidential balance fields
+            tx = vault.withdraw(
+                {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)});
+            env(tx);
+
+            // The share MPToken should still exist because the
+            // withdrawal failed due to confidential balance obligations
+            shareMpt = env.le(keylet::mptoken(share, depositor.id()));
+            BEAST_EXPECT(shareMpt != nullptr);
+        }
+    }
+
+    void
+    testConvertBack(FeatureBitset features)
+    {
+        testcase("Convert back");
+        using namespace test::jtx;
+
+        // Basic convert back test
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+            });
+        }
+
+        // Edge case: minimum amount (1)
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 2, .convertAmount = 2}}};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 1,
+            });
+        }
+
+        // Edge case: kMaxMpTokenAmount
+        // Using raw JSON to avoid automatic decryption checks in MPTTester
+        // which don't work for very large amounts (brute-force decryption is slow)
+        // TODO: improve this test once there is bounded decryption or optimized decryption for
+        // large amounts
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, kMaxMpTokenAmount);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            // Convert kMaxMpTokenAmount to confidential using raw JSON
+            Buffer const convertBlindingFactor = generateBlindingFactor();
+            auto const convertHolderCiphertext =
+                mptAlice.encryptAmount(bob, kMaxMpTokenAmount, convertBlindingFactor);
+            auto const convertIssuerCiphertext =
+                mptAlice.encryptAmount(alice, kMaxMpTokenAmount, convertBlindingFactor);
+            auto const convertContextHash =
+                getConvertContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob));
+            auto const schnorrProof = requireOptional(
+                mptAlice.getSchnorrProof(bob, convertContextHash), "Missing schnorr proof");
+
+            {
+                json::Value jv;
+                jv[jss::Account] = bob.human();
+                jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
+                jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+                jv[sfMPTAmount.jsonName] = std::to_string(kMaxMpTokenAmount);
+                jv[sfHolderEncryptionKey.jsonName] =
+                    strHex(requireOptional(mptAlice.getPubKey(bob), "Missing holder public key"));
+                jv[sfHolderEncryptedAmount.jsonName] = strHex(convertHolderCiphertext);
+                jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertIssuerCiphertext);
+                jv[sfBlindingFactor.jsonName] = strHex(convertBlindingFactor);
+                jv[sfZKProof.jsonName] = strHex(schnorrProof);
+
+                env(jv, Ter(tesSUCCESS));
+            }
+
+            // Merge inbox using raw JSON - moves funds from inbox to spending balance
+            {
+                json::Value jv;
+                jv[jss::Account] = bob.human();
+                jv[jss::TransactionType] = jss::ConfidentialMPTMergeInbox;
+                jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+
+                env(jv, Ter(tesSUCCESS));
+            }
+
+            // ConvertBack kMaxMpTokenAmount - 1 using raw JSON
+            // After convert + merge, spending balance = kMaxMpTokenAmount
+            // We convert back kMaxMpTokenAmount - 1 to leave remainder of 1
+            std::uint64_t const convertBackAmt = kMaxMpTokenAmount - 1;
+
+            Buffer const convertBackBlindingFactor = generateBlindingFactor();
+            auto const convertBackHolderCiphertext =
+                mptAlice.encryptAmount(bob, convertBackAmt, convertBackBlindingFactor);
+            auto const convertBackIssuerCiphertext =
+                mptAlice.encryptAmount(alice, convertBackAmt, convertBackBlindingFactor);
+
+            // Get the encrypted spending balance from ledger (no decryption needed)
+            auto const encryptedSpendingBalance = requireOptional(
+                mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+                "Missing encrypted spending balance");
+
+            // Generate pedersen commitment for the known spending balance
+            Buffer const pcBlindingFactor = generateBlindingFactor();
+            Buffer const pedersenCommitment =
+                mptAlice.getPedersenCommitment(kMaxMpTokenAmount, pcBlindingFactor);
+
+            // Generate the proof using known spending balance value
+            auto const version = mptAlice.getMPTokenVersion(bob);
+            uint256 const convertBackContextHash =
+                getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                convertBackAmt,
+                convertBackContextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = kMaxMpTokenAmount,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            {
+                json::Value jv;
+                jv[jss::Account] = bob.human();
+                jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack;
+                jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+                jv[sfMPTAmount.jsonName] = std::to_string(convertBackAmt);
+                jv[sfHolderEncryptedAmount.jsonName] = strHex(convertBackHolderCiphertext);
+                jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertBackIssuerCiphertext);
+                jv[sfBlindingFactor.jsonName] = strHex(convertBackBlindingFactor);
+                jv[sfBalanceCommitment.jsonName] = strHex(pedersenCommitment);
+                jv[sfZKProof.jsonName] = strHex(proof);
+
+                env(jv, Ter(tesSUCCESS));
+            }
+
+            // Verify the public balance was restored (minus 1 remaining in confidential)
+            env.require(MptBalance(mptAlice, bob, convertBackAmt));
+        }
+    }
+
+    void
+    testConvertBackWithAuditor(FeatureBitset features)
+    {
+        testcase("Convert back with auditor");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const auditor("auditor");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 40}},
+            tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            auditor};
+        auto& mptAlice = confEnv.mpt;
+
+        mptAlice.convertBack({
+            .account = bob,
+            .amt = 30,
+        });
+    }
+
+    void
+    testConvertBackPreflight(FeatureBitset features)
+    {
+        testcase("Convert back preflight");
+        using namespace test::jtx;
+
+        {
+            Env env{*this, features - featureConfidentialTransfer};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .err = temDISABLED,
+            });
+        }
+
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.convertBack({
+                .account = alice,
+                .amt = 30,
+                .err = temMALFORMED,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 0,
+                .err = temBAD_AMOUNT,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = kMaxMpTokenAmount + 1,
+                .err = temBAD_AMOUNT,
+            });
+
+            // Balance commitment has correct length but invalid EC point data
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .pedersenCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength),
+                .err = temMALFORMED,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .holderEncryptedAmt = Buffer{},
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .issuerEncryptedAmt = Buffer{},
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .holderEncryptedAmt = getBadCiphertext(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .issuerEncryptedAmt = getBadCiphertext(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .auditorEncryptedAmt = gMakeZeroBuffer(10),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .auditorEncryptedAmt = getBadCiphertext(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // invalid proof length
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .proof = Buffer{},
+                .err = temMALFORMED,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .proof = gMakeZeroBuffer(100),
+                .err = temMALFORMED,
+            });
+        }
+    }
+
+    void
+    testConvertBackPreclaim(FeatureBitset features)
+    {
+        testcase("Convert back preclaim");
+        using namespace test::jtx;
+
+        // issuance does not exist
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.destroy();
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // tfMPTCanHoldConfidentialBalance is not set on issuance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // no mptoken
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .err = tecOBJECT_NOT_FOUND,
+            });
+        }
+
+        // mptoken exists but lacks confidential fields
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // Bob's MPToken lacks the confidential fields
+            auto const sleBobMpt = env.le(keylet::mptoken(mptAlice.issuanceID(), bob.id()));
+            BEAST_EXPECT(sleBobMpt);
+            BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfHolderEncryptionKey));
+            BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfConfidentialBalanceSpending));
+            BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfIssuerEncryptedBalance));
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 30,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // bob tries to convert back more than COA
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 300,
+                .err = tecINSUFFICIENT_FUNDS,
+            });
+        }
+
+        // cannot convert if locked or unauth
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
+                    tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 40,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTLock,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                .err = tecLOCKED,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnlock,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+            });
+
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnauthorize,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                .err = tecNO_AUTH,
+            });
+
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+            });
+        }
+
+        // Verification of holder and issuer ciphertexts during convertBack
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
+            auto& mptAlice = confEnv.mpt;
+
+            // Holder encrypted amount is valid format but mathematically incorrect for this
+            // convertBack
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                .holderEncryptedAmt = getTrivialCiphertext(),
+                .err = tecBAD_PROOF,
+            });
+
+            // Issuer encrypted amount is valid format but mathematically incorrect for this
+            // convertBack
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                .issuerEncryptedAmt = getTrivialCiphertext(),
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Alice has NOT set an auditor key, but Bob provides
+        // auditorEncryptedAmt
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                // Provide valid ciphertext to pass preflight
+                .auditorEncryptedAmt = getTrivialCiphertext(),
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // we set the auditor key, but convertBack omits auditorEncryptedAmt
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const auditor("auditor");
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50}},
+                tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+                auditor};
+            auto& mptAlice = confEnv.mpt;
+
+            // ConvertBack WITHOUT auditorEncryptedAmt
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                .fillAuditorEncryptedAmt = false,
+                .err = tecNO_PERMISSION,
+            });
+
+            // ConvertBack where auditor ciphertext mathematically
+            // correct, but contains invalid data (mismatching amount).
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = 10,
+                .auditorEncryptedAmt = getTrivialCiphertext(),
+                .err = tecBAD_PROOF,
+            });
+        }
+    }
+
+    void
+    testClawback(FeatureBitset features)
+    {
+        testcase("test ConfidentialMPTClawback");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+        MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}});
+
+        mptAlice.create({
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
+                tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({
+            .account = bob,
+        });
+        mptAlice.pay(alice, bob, 100);
+        mptAlice.authorize({
+            .account = carol,
+        });
+        mptAlice.pay(alice, carol, 200);
+        mptAlice.authorize({
+            .account = dave,
+        });
+        mptAlice.pay(alice, dave, 300);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.generateKeyPair(dave);
+        mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+        // setup bob.
+        // after setup, bob's spending balance is 60, inbox balance is 0.
+        {
+            // bob converts 60 to confidential
+            mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)});
+
+            // bob merge inbox
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+        }
+
+        // setup carol.
+        // after setup, carol's spending balance is 120, inbox balance is 0.
+        {
+            // carol converts 120 to confidential
+            mptAlice.convert(
+                {.account = carol, .amt = 120, .holderPubKey = mptAlice.getPubKey(carol)});
+
+            // carol merge inbox
+            mptAlice.mergeInbox({
+                .account = carol,
+            });
+        }
+
+        // setup dave.
+        // dave will not merge inbox.
+        // after setup, dave's inbox balance is 200, spending balance is 0.
+        mptAlice.convert({.account = dave, .amt = 200, .holderPubKey = mptAlice.getPubKey(dave)});
+
+        // setup: carol confidential send 50 to bob.
+        // after send, bob's inbox balance is 50, spending balance
+        // remains 60. carol's inbox balance remains 0, spending balance
+        // drops to 70.
+        mptAlice.send({
+            .account = carol,
+            .dest = bob,
+            .amt = 50,
+        });
+
+        // Confidential clawback is burn/reduce outstanding amount.
+        // The holder public balance is unchanged, and OA/COA decrease.
+        auto const preBobPublicBalance = mptAlice.getBalance(bob);
+        auto const preOutstandingAmount = mptAlice.getIssuanceOutstandingBalance();
+        auto const preConfidentialOutstandingAmount = mptAlice.getIssuanceConfidentialBalance();
+        BEAST_EXPECT(!env.le(keylet::mptoken(mptAlice.issuanceID(), alice.id())));
+
+        // alice clawback all confidential balance from bob, 110 in total.
+        // bob has balance in both inbox and spending. These balances should
+        // become zero after clawback, which is verified in the
+        // confidentialClaw function.
+        mptAlice.confidentialClaw({
+            .account = alice,
+            .holder = bob,
+            .amt = 110,
+        });
+        BEAST_EXPECT(mptAlice.getBalance(bob) == preBobPublicBalance);
+        auto const postOutstandingAmount = mptAlice.getIssuanceOutstandingBalance();
+        BEAST_EXPECT(
+            preOutstandingAmount && postOutstandingAmount &&
+            *postOutstandingAmount == *preOutstandingAmount - 110);
+        BEAST_EXPECT(
+            mptAlice.getIssuanceConfidentialBalance() == preConfidentialOutstandingAmount - 110);
+        BEAST_EXPECT(!env.le(keylet::mptoken(mptAlice.issuanceID(), alice.id())));
+
+        // alice clawback all confidential balance from carol, which is 70.
+        // carol only has balance in spending.
+        mptAlice.confidentialClaw({
+            .account = alice,
+            .holder = carol,
+            .amt = 70,
+        });
+
+        // alice clawback all confidential balance from dave, which is 200.
+        // dave only has balance in inbox.
+        mptAlice.confidentialClaw({
+            .account = alice,
+            .holder = dave,
+            .amt = 200,
+        });
+    }
+
+    void
+    testClawbackWithAuditor(FeatureBitset features)
+    {
+        testcase("test ConfidentialMPTClawback with auditor");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const dave("dave");
+        Account const auditor("auditor");
+        MPTTester mptAlice(
+            env,
+            alice,
+            {
+                .holders = {bob, carol, dave},
+                .auditor = auditor,
+            });
+
+        mptAlice.create({
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
+                tfMPTCanHoldConfidentialBalance,
+        });
+        mptAlice.authorize({
+            .account = bob,
+        });
+        mptAlice.pay(alice, bob, 100);
+        mptAlice.authorize({
+            .account = carol,
+        });
+        mptAlice.pay(alice, carol, 200);
+        mptAlice.authorize({
+            .account = dave,
+        });
+        mptAlice.pay(alice, dave, 300);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.generateKeyPair(dave);
+        mptAlice.generateKeyPair(auditor);
+        mptAlice.set(
+            {.account = alice,
+             .issuerPubKey = mptAlice.getPubKey(alice),
+             .auditorPubKey = mptAlice.getPubKey(auditor)});
+
+        // setup bob.
+        // after setup, bob's spending balance is 60, inbox balance is 0.
+        {
+            // bob converts 60 to confidential
+            mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)});
+
+            // bob merge inbox
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+        }
+
+        // setup carol.
+        // after setup, carol's spending balance is 120, inbox balance is 0.
+        {
+            // carol converts 120 to confidential
+            mptAlice.convert(
+                {.account = carol, .amt = 120, .holderPubKey = mptAlice.getPubKey(carol)});
+
+            // carol merge inbox
+            mptAlice.mergeInbox({
+                .account = carol,
+            });
+        }
+
+        // setup dave.
+        // dave will not merge inbox.
+        // after setup, dave's inbox balance is 200, spending balance is 0.
+        mptAlice.convert({.account = dave, .amt = 200, .holderPubKey = mptAlice.getPubKey(dave)});
+
+        // setup: carol confidential send 50 to bob.
+        // after send, bob's inbox balance is 50, spending balance
+        // remains 60. carol's inbox balance remains 0, spending balance
+        // drops to 70.
+        mptAlice.send({
+            .account = carol,
+            .dest = bob,
+            .amt = 50,
+        });
+
+        // alice clawback all confidential balance from bob, 110 in total.
+        // bob has balance in both inbox and spending. These balances should
+        // become zero after clawback, which is verified in the
+        // confidentialClaw function.
+        mptAlice.confidentialClaw({
+            .account = alice,
+            .holder = bob,
+            .amt = 110,
+        });
+
+        // alice clawback all confidential balance from carol, which is 70.
+        // carol only has balance in spending.
+        mptAlice.confidentialClaw({
+            .account = alice,
+            .holder = carol,
+            .amt = 70,
+        });
+
+        // alice clawback all confidential balance from dave, which is 200.
+        // dave only has balance in inbox.
+        mptAlice.confidentialClaw({
+            .account = alice,
+            .holder = dave,
+            .amt = 200,
+        });
+    }
+
+    void
+    testClawbackInvalidProofContextBinding(FeatureBitset features)
+    {
+        testcase("ConfidentialMPTClawback context binding");
+        using namespace test::jtx;
+
+        auto runBadProof = [&](auto makeContextHash) {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60}},
+                tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
+                    tfMPTCanHoldConfidentialBalance};
+            auto& mptAlice = confEnv.mpt;
+
+            auto const privKey = mptAlice.getPrivKey(alice);
+            if (!BEAST_EXPECT(privKey.has_value()))
+                return;
+
+            auto const proof = mptAlice.getClawbackProof(
+                bob,
+                60,
+                requireOptionalRef(privKey, "Missing private key"),
+                makeContextHash(env, mptAlice, alice, bob, carol));
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+                .proof = strHex(requireOptional(proof, "Missing proof")),
+                .err = tecBAD_PROOF,
+            });
+        };
+
+        // Wrong account (issuer) in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const& alice,
+                        Account const& bob,
+                        Account const& carol) {
+            return getClawbackContextHash(carol.id(), mpt.issuanceID(), env.seq(alice), bob.id());
+        });
+
+        // Wrong issuance ID in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const&,
+                        Account const& alice,
+                        Account const& bob,
+                        Account const&) {
+            return getClawbackContextHash(
+                alice.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(alice), bob.id());
+        });
+
+        // Wrong transaction sequence in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const& alice,
+                        Account const& bob,
+                        Account const&) {
+            return getClawbackContextHash(
+                alice.id(), mpt.issuanceID(), env.seq(alice) + 1, bob.id());
+        });
+
+        // Wrong holder in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const& alice,
+                        Account const&,
+                        Account const& carol) {
+            return getClawbackContextHash(alice.id(), mpt.issuanceID(), env.seq(alice), carol.id());
+        });
+    }
+
+    // Bob creates the AMM, but Bob is not the MPT holder checked below.
+    // The AMM has its own pseudo-account (`ammHolder`) that can hold the
+    // public MPT pool balance. That pseudo-account cannot normally
+    // initialize confidential state because the confidential txn's must be
+    // signed by sfAccount, and the AMM pseudo-account has no signing key.
+    // So this is a construction/impossibility test: public AMM MPT state exists
+    // but the corresponding confidential AMM clawback flow is not normally reachable.
+    void
+    testClawbackPreflight(FeatureBitset features)
+    {
+        testcase("test ConfidentialMPTClawback Preflight");
+        using namespace test::jtx;
+
+        // test feature disabled
+        {
+            Env env{*this, features - featureConfidentialTransfer};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create();
+            mptAlice.authorize({
+                .account = bob,
+            });
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 10,
+                .proof = "123",
+                .err = temDISABLED,
+            });
+        }
+
+        // test malformed
+        {
+            // set up
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+
+            // only issuer can clawback
+            mptAlice.confidentialClaw({
+                .account = carol,
+                .holder = bob,
+                .amt = 10,
+                .err = temMALFORMED,
+            });
+
+            // invalid issuance ID, whose issuer is not alice
+            {
+                json::Value jv;
+                jv[jss::Account] = alice.human();
+                jv[sfHolder] = bob.human();
+                jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
+                jv[sfMPTAmount] = std::to_string(10);
+                jv[sfZKProof] = "123";
+
+                // wrong issuance ID
+                jv[sfMPTokenIssuanceID] = "00000004AE123A8556F3CF91154711376AFB0F894F832B3E";
+
+                env(jv, Ter(temMALFORMED));
+            }
+
+            // issuer cannot clawback from self
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = alice,
+                .amt = 10,
+                .err = temMALFORMED,
+            });
+
+            // invalid amount
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 0,
+                .err = temBAD_AMOUNT,
+            });
+
+            // invalid proof length
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 10,
+                .proof = "123",
+                .err = temMALFORMED,
+            });
+        }
+    }
+
+    void
+    testClawbackPreclaim(FeatureBitset features)
+    {
+        testcase("Clawback Preclaim Errors");
+        using namespace test::jtx;
+
+        {
+            // set up, alice is the issuer, bob and carol are authorized
+            // holders. dave is not authorized. bob has confidential
+            // balance, carol does not.
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            Account const dave("dave");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}});
+
+            mptAlice.create({
+                .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth |
+                    tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+            });
+            mptAlice.authorize({
+                .account = carol,
+            });
+            mptAlice.authorize({
+                .account = alice,
+                .holder = carol,
+            });
+
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 60,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+
+            // holder does not exist
+            {
+                Account const unknown("unknown");
+                mptAlice.confidentialClaw({
+                    .account = alice,
+                    .holder = unknown,
+                    .amt = 10,
+                    .err = tecNO_TARGET,
+                });
+            }
+
+            // dave does not hold mpt at all, no MPT object
+            {
+                mptAlice.confidentialClaw({
+                    .account = alice,
+                    .holder = dave,
+                    .amt = 10,
+                    .err = tecOBJECT_NOT_FOUND,
+                });
+            }
+
+            // carol has no confidential balance
+            {
+                mptAlice.confidentialClaw({
+                    .account = alice,
+                    .holder = carol,
+                    .amt = 10,
+                    .err = tecNO_PERMISSION,
+                });
+            }
+        }
+
+        // lsfMPTCanClawback not set
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // no issuer key
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+            mptAlice.create({
+                .flags = tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 10,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // issuance not found
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+            mptAlice.create({
+                .flags = tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // destroy the issuance
+            mptAlice.destroy();
+
+            json::Value jv;
+            jv[jss::Account] = alice.human();
+            jv[sfHolder] = bob.human();
+            jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
+            jv[sfMPTAmount] = std::to_string(10);
+            std::string const dummyProof(kEcClawbackProofLength * 2, '0');
+            jv[sfZKProof] = dummyProof;
+            jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
+
+            env(jv, Ter(tecOBJECT_NOT_FOUND));
+        }
+
+        // After setup, bob has confidential balance 60 in spending.
+        std::uint32_t const setupFlags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth |
+            tfMPTCanLock | tfMPTCanHoldConfidentialBalance;
+        std::string const dummyClawbackProof(kEcClawbackProofLength * 2, '0');
+
+        auto removeMPTokenField =
+            [&](Env& env, MPTTester const& mpt, Account const& holder, SField const& field) {
+                BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+                    auto const sle = std::const_pointer_cast(
+                        view.read(keylet::mptoken(mpt.issuanceID(), holder.id())));
+                    if (!sle)
+                        return false;
+
+                    sle->makeFieldAbsent(field);
+                    view.rawReplace(sle);
+                    return true;
+                }));
+            };
+
+        // After global COA is drained to zero, a further confidential clawback
+        // fails because the amount exceeds the remaining confidential
+        // outstanding amount.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+            });
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 1,
+                .proof = dummyClawbackProof,
+                .err = tecINSUFFICIENT_FUNDS,
+            });
+        }
+
+        // Missing issuer encrypted balance should fail before proof
+        // verification.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+
+            removeMPTokenField(env, mptAlice, bob, sfIssuerEncryptedBalance);
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+                .proof = dummyClawbackProof,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Missing holder encryption key should fail before proof verification.
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+
+            removeMPTokenField(env, mptAlice, bob, sfHolderEncryptionKey);
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+                .proof = dummyClawbackProof,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // lock should not block clawback. lock bob individually
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+            mptAlice.set({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTLock,
+            });
+
+            // clawback should still work
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+            });
+        }
+
+        // lock globally
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+            mptAlice.set({
+                .account = alice,
+                .flags = tfMPTLock,
+            });
+
+            // clawback should still work
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+            });
+        }
+
+        // unauthorize should not block clawback
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+
+            // unauthorize bob
+            mptAlice.authorize({
+                .account = alice,
+                .holder = bob,
+                .flags = tfMPTUnauthorize,
+            });
+            // clawback should still work
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 60,
+            });
+        }
+
+        // insufficient funds, clawback amount exceeding confidential
+        // outstanding amount
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
+            auto& mptAlice = confEnv.mpt;
+
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 10000,
+                .err = tecINSUFFICIENT_FUNDS,
+            });
+        }
+    }
+
+    void
+    testClawbackProof(FeatureBitset features)
+    {
+        testcase("ConfidentialMPTClawback Proof");
+        using namespace test::jtx;
+
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+
+        // lambda function to set up MPT with alice as issuer, bob and carol
+        // as authorized holders, and fund 1000 mpt to bob and 2000 mpt to
+        // carol.
+        auto setupEnv = [&](Env& env) -> MPTTester {
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
+            });
+
+            for (auto const& [acct, amt] : {std::pair{bob, 1000}, {carol, 2000}})
+            {
+                mptAlice.authorize({
+                    .account = acct,
+                });
+                mptAlice.pay(alice, acct, amt);
+                mptAlice.generateKeyPair(acct);
+            }
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            return mptAlice;
+        };
+
+        // lambda function to test a set of bad clawback amounts that should
+        // return tecBAD_PROOF
+        auto checkBadProofs =
+            [&](MPTTester& mpt, Account const& holder, std::initializer_list amts) {
+                for (auto const badAmt : amts)
+                {
+                    mpt.confidentialClaw({
+                        .account = alice,
+                        .holder = holder,
+                        .amt = badAmt,
+                        .err = tecBAD_PROOF,
+                    });
+                }
+            };
+
+        // SCENARIO 1: clawback from inbox only or spending only balances.
+        // bob converts 500 and merge inbox,
+        // carol converts 1000, but not merge inbox.
+        // after setup, bob has 500 in spending, carol has 1000 in inbox.
+        {
+            Env env{*this, features};
+            auto mptAlice = setupEnv(env);
+
+            // bob converts and merges
+            mptAlice.convert({.account = bob, .amt = 500, .holderPubKey = mptAlice.getPubKey(bob)});
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+            // carol converts without merge
+            mptAlice.convert(
+                {.account = carol, .amt = 1000, .holderPubKey = mptAlice.getPubKey(carol)});
+
+            // verify proof fails with invalid clawback amount
+            // bob: 500 in Spending, 0 in Inbox
+            checkBadProofs(
+                mptAlice,
+                bob,
+                {
+                    1,
+                    10,
+                    70,
+                    100,
+                    110,
+                    200,
+                    499,
+                    501,
+                    600,
+                });
+
+            // carol: 1000 in Inbox, 0 in Spending
+            checkBadProofs(
+                mptAlice,
+                carol,
+                {
+                    1,
+                    10,
+                    50,
+                    500,
+                    777,
+                    850,
+                    999,
+                    1001,
+                    1200,
+                });
+
+            // clawback with correct amount that passes proof verification
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 500,
+            });
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = carol,
+                .amt = 1000,
+            });
+        }
+
+        // SCENARIO 2: clawback from mixed inbox and spending balances.
+        // bob converts 300 to confidential and merge inbox,
+        // carol converts 400 to confidential and merge inbox,
+        // bob sends 100 to carol, carol sends 100 to bob.
+        // After setup, bob has 100 in inbox and 200 in spending;
+        // carol has 100 in inbox and 300 in spending.
+        {
+            Env env{*this, features};
+            auto mptAlice = setupEnv(env);
+
+            mptAlice.convert({.account = bob, .amt = 300, .holderPubKey = mptAlice.getPubKey(bob)});
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+            mptAlice.convert(
+                {.account = carol, .amt = 400, .holderPubKey = mptAlice.getPubKey(carol)});
+            mptAlice.mergeInbox({
+                .account = carol,
+            });
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 100,
+            });
+            mptAlice.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 100,
+            });
+
+            // verify proof fails with invalid clawback amount
+            // bob: 100 in inbox, 200 in spending
+            checkBadProofs(
+                mptAlice,
+                bob,
+                {
+                    1,
+                    10,
+                    50,
+                    100,
+                    200,
+                    299,
+                    301,
+                    400,
+                });
+
+            // proof failure for incorrect amount when clawbacking from
+            // carol carol: 100 in inbox, 300 in spending
+            checkBadProofs(
+                mptAlice,
+                carol,
+                {
+                    1,
+                    10,
+                    50,
+                    100,
+                    300,
+                    399,
+                    401,
+                    501,
+                });
+
+            // clawback with correct amount that passes proof verification
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 300,
+            });
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = carol,
+                .amt = 400,
+            });
+        }
+
+        // SCENARIO 3: the clawback proof omits the holder's confidential
+        // balance version. A proof generated before the version advances is
+        // still accepted, because getClawbackContextHash has no version
+        // component.
+        {
+            Env env{*this, features};
+            auto mptAlice = setupEnv(env);
+
+            mptAlice.convert({.account = bob, .amt = 500, .holderPubKey = mptAlice.getPubKey(bob)});
+            mptAlice.mergeInbox({
+                .account = bob,
+            });
+
+            auto const privKey = mptAlice.getPrivKey(alice);
+            if (!BEAST_EXPECT(privKey.has_value()))
+                return;
+
+            auto const proof = mptAlice.getClawbackProof(
+                bob,
+                500,
+                requireOptionalRef(privKey, "Missing private key"),
+                getClawbackContextHash(
+                    alice.id(), mptAlice.issuanceID(), env.seq(alice), bob.id()));
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            // Advance bob's balance version after the proof is generated. An
+            // empty-inbox merge leaves the balance unchanged but still bumps
+            // sfConfidentialBalanceVersion.
+            auto const versionBefore = mptAlice.getMPTokenVersion(bob);
+            mptAlice.mergeInbox({.account = bob});
+            BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) != versionBefore);
+
+            // The stale-version proof is still accepted.
+            mptAlice.confidentialClaw({
+                .account = alice,
+                .holder = bob,
+                .amt = 500,
+                .proof = strHex(requireOptional(proof, "Missing proof")),
+            });
+        }
+    }
+
+    void
+    testPublicTransfersAfterClearingConfidentialFlag(FeatureBitset features)
+    {
+        testcase("Public transfers after clearing Confidential Flag");
+        using namespace test::jtx;
+
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+
+        // After clearing the confidential flag, all four public MPT operations
+        // must succeed regardless of which confidential path left encrypted-zero
+        // fields on bob's MPToken.
+        auto runPublicPayments = [&](MPTTester& mpt) {
+            mpt.pay(bob, carol, 10);
+            mpt.pay(carol, bob, 5);
+            mpt.pay(alice, bob, 1);
+            mpt.pay(carol, alice, 5);
+        };
+
+        auto drainAndDeleteBobMPToken = [&](Env& env, MPTTester& mpt) {
+            auto const bobBalance = mpt.getBalance(bob);
+            BEAST_EXPECT(bobBalance > 0);
+
+            mpt.pay(bob, alice, bobBalance);
+            BEAST_EXPECT(mpt.getBalance(bob) == 0);
+
+            mpt.authorize({.account = bob, .flags = tfMPTUnauthorize});
+            BEAST_EXPECT(!env.le(keylet::mptoken(mpt.issuanceID(), bob.id())));
+        };
+
+        // Alice pays Bob 100 public, Bob converts 50 confidential
+        // Bob converts 50 back to public, and make sure can receive public payments
+        {
+            Env env{*this, features};
+            ConfidentialEnv ct{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50}},
+                tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+
+            env.fund(XRP(1'000), carol);
+            ct.mpt.authorize({.account = carol});
+            ct.mpt.pay(alice, carol, 50);
+
+            ct.mpt.convertBack({.account = bob, .amt = 50});
+
+            runPublicPayments(ct.mpt);
+            drainAndDeleteBobMPToken(env, ct.mpt);
+        }
+
+        // Same path as above but with Auditor
+        {
+            Env env{*this, features};
+            Account const auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({.account = bob});
+            mptAlice.authorize({.account = carol});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(auditor);
+            mptAlice.set(
+                {.account = alice,
+                 .issuerPubKey = mptAlice.getPubKey(alice),
+                 .auditorPubKey = mptAlice.getPubKey(auditor)});
+
+            mptAlice.convert({
+                .account = bob,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(bob),
+            });
+            mptAlice.mergeInbox({.account = bob});
+            mptAlice.convertBack({.account = bob, .amt = 50});
+
+            runPublicPayments(mptAlice);
+            drainAndDeleteBobMPToken(env, mptAlice);
+        }
+
+        // Confidential clawback leaves encrypted-zero fields;
+        // the public balance remaining after the clawback must stay usable.
+        {
+            Env env{*this, features};
+            ConfidentialEnv ct{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 50}},
+                tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance};
+
+            env.fund(XRP(1'000), carol);
+            ct.mpt.authorize({.account = carol});
+            ct.mpt.pay(alice, carol, 50);
+
+            ct.mpt.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
+
+            runPublicPayments(ct.mpt);
+            drainAndDeleteBobMPToken(env, ct.mpt);
+        }
+    }
+
+    void
+    testMutatePrivacy(FeatureBitset features)
+    {
+        testcase("mutate lsfMPTCanHoldConfidentialBalance");
+        using namespace test::jtx;
+
+        // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance
+        // when featureDynamicMPT is disabled
+        {
+            Env env{*this, features - featureDynamicMPT};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 0,
+                .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance,
+                .err = temDISABLED,
+            });
+        }
+
+        // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance when
+        // featureConfidentialTransfer is disabled
+        {
+            Env env{*this, features - featureConfidentialTransfer};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 0,
+                .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance,
+                .err = temDISABLED,
+            });
+        }
+
+        // if lsmfMPTCannotEnableCanHoldConfidentialBalance is set, can not set/clear
+        // lsfMPTCanHoldConfidentialBalance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer,
+                .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance,
+            });
+
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+                .err = tecNO_PERMISSION,
+            });
+        }
+
+        // Toggle lsfMPTCanHoldConfidentialBalance
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+                .mutableFlags = tmfMPTCanEnableCanLock,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            auto holderPubKeySet = false;
+            auto verifyToggle = [&](TER expectedResult, uint64_t amt) {
+                if (!holderPubKeySet)
+                {
+                    mptAlice.convert({
+                        .account = bob,
+                        .amt = amt,
+                        .holderPubKey = mptAlice.getPubKey(bob),
+                        .err = expectedResult,
+                    });
+                }
+                else
+                {
+                    mptAlice.convert({
+                        .account = bob,
+                        .amt = amt,
+                        .err = expectedResult,
+                    });
+                }
+
+                if (expectedResult == tesSUCCESS)
+                {
+                    holderPubKeySet = true;
+                    mptAlice.mergeInbox({
+                        .account = bob,
+                    });
+
+                    // make sure there's no confidential outstanding balance
+                    // for the next toggle test
+                    mptAlice.convertBack({
+                        .account = bob,
+                        .amt = amt,
+                    });
+                }
+            };
+
+            // set lsfMPTCanHoldConfidentialBalance, but no effect because
+            // lsfMPTCanHoldConfidentialBalance was already set
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+            });
+            verifyToggle(tesSUCCESS, 10);
+
+            // set tmfMPTSetCanHoldConfidentialBalance again
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+            });
+            verifyToggle(tesSUCCESS, 30);
+        }
+
+        // can not mutate lsfPrivacy when there's confidential
+        // outstanding amount
+        {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+            // lsmfMPTCannotEnableCanHoldConfidentialBalance is false by default,
+            // so that lsfMPTCanHoldConfidentialBalance can be mutated
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({
+                .account = bob,
+            });
+            mptAlice.pay(alice, bob, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // bob convert 50 to confidential
+            mptAlice.convert({.account = bob, .amt = 50, .holderPubKey = mptAlice.getPubKey(bob)});
+
+            // set lsfMPTCanHoldConfidentialBalance should fail because of
+            // confidential outstanding balance
+            mptAlice.set({
+                .account = alice,
+                .mutableFlags = tmfMPTSetCanHoldConfidentialBalance,
+                .err = tecNO_PERMISSION,
+            });
+        }
+    }
+
+    void
+    testConvertBackPedersenProof(FeatureBitset features)
+    {
+        testcase("Convert back pedersen proof");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        ConfidentialEnv confEnv{
+            env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // for ease of understanding, generate all the fields here instead of
+        // autofilling
+        uint64_t const amt = 10;
+        Buffer const blindingFactor = generateBlindingFactor();
+        Buffer const pcBlindingFactor = generateBlindingFactor();
+
+        auto const spendingBalance = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing spending balance");
+        auto const encryptedSpendingBalance = requireOptional(
+            mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing encrypted spending balance");
+        BEAST_EXPECT(!encryptedSpendingBalance.empty());
+
+        Buffer const pedersenCommitment =
+            mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
+        Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
+        Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
+        auto const version = mptAlice.getMPTokenVersion(bob);
+
+        // These tests verify that the compact ConvertBack proof validation
+        // correctly rejects proofs generated with incorrect parameters.
+        // The compact proof simultaneously verifies balance ownership,
+        // commitment linkage, and that remaining balance is non-negative.
+
+        // Test 1: Proof generated with wrong pedersen commitment value.
+        // The proof uses PC(1, rho) but the transaction submits PC(balance, rho).
+        // Verification fails because the proof doesn't match the submitted commitment.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+            Buffer const badPedersenCommitment =
+                mptAlice.getPedersenCommitment(1, pcBlindingFactor);
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = badPedersenCommitment,  // wrong pedersen commitment
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 2: Proof generated with wrong blinding factor (rho).
+        // The pedersen commitment PC = balance*G + rho*H requires the same rho
+        // used in proof generation. Using a different rho breaks the linkage.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = generateBlindingFactor(),  // wrong blinding factor
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 3: Proof generated with wrong balance value.
+        // The proof claims balance=1 but the encrypted spending balance contains
+        // the actual balance. Verification fails because the values don't match.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = 1,  // wrong balance
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 4: Correct proof but wrong pedersen commitment in transaction.
+        // The proof is generated correctly, but the transaction submits a
+        // different pedersen commitment. Verification fails because the
+        // submitted commitment doesn't match what the proof was generated for.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+            Buffer const badPedersenCommitment =
+                mptAlice.getPedersenCommitment(1, pcBlindingFactor);
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = badPedersenCommitment,  // wrong pedersen commitment
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 5: Proof generated with wrong context hash.
+        // The context hash binds the proof to a specific transaction (account,
+        // sequence, issuanceID, amount, version). Using a different context hash
+        // makes the proof invalid for this transaction, preventing replay attacks.
+        {
+            uint256 const badContextHash{1};
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                badContextHash,  // wrong context hash
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 6: Correct proof to verify the test setup is valid.
+        // All parameters are correct, so the transaction should succeed.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+            });
+        }
+    }
+
+    void
+    testConvertBackBulletproof(FeatureBitset features)
+    {
+        testcase("Convert back bulletproof");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        ConfidentialEnv confEnv{
+            env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // for ease of understanding, generate all the fields here instead of
+        // autofilling
+        uint64_t const amt = 10;
+        Buffer const blindingFactor = generateBlindingFactor();
+        Buffer const pcBlindingFactor = generateBlindingFactor();
+
+        auto const spendingBalance = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing spending balance");
+        auto const encryptedSpendingBalance = requireOptional(
+            mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing encrypted spending balance");
+        BEAST_EXPECT(!encryptedSpendingBalance.empty());
+
+        Buffer const pedersenCommitment =
+            mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
+        Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
+        Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
+        auto const version = mptAlice.getMPTokenVersion(bob);
+
+        // These tests verify that the compact ConvertBack proof (sigma + bulletproof)
+        // correctly rejects proofs generated with incorrect parameters.
+        // The compact proof simultaneously verifies balance ownership, commitment
+        // linkage, and that the remaining balance is non-negative.
+
+        // Test 1: Proof generated with wrong balance value.
+        // The sigma proof claims balance=1 but the spending balance contains the
+        // actual balance. The compact proof's balance-linkage check fails.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = 1,  // wrong balance (actual balance is ~40)
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 2: Proof generated with wrong blinding factor (rho).
+        // The compact sigma proof must use the same blinding factor (rho) as the
+        // Pedersen commitment PC = balance*G + rho*H. Using a different rho
+        // creates an inconsistency the verifier detects.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = generateBlindingFactor(),  // wrong blinding factor
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 3: Proof generated with wrong context hash.
+        // The context hash binds the proof to a specific transaction (account,
+        // sequence, issuanceID, amount, version). Using a different context hash
+        // makes the proof invalid for this transaction, preventing replay attacks.
+        {
+            uint256 const badContextHash{1};
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                badContextHash,  // wrong context hash
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Test 4: Correct proof to verify the test setup is valid.
+        // All parameters are correct, so the transaction should succeed.
+        {
+            uint256 const contextHash =
+                getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+            });
+        }
+    }
+
+    // A convert-back proof is bound to (account, issuance, sequence, version) via
+    // the Fiat-Shamir context hash. Crafting a proof against any single wrong
+    // variable and submitting it with the real parameters must be rejected
+    // with tecBAD_PROOF
+    void
+    testConvertBackInvalidProofContextBinding(FeatureBitset features)
+    {
+        testcase("ConvertBack proof context binding");
+        using namespace test::jtx;
+
+        auto runBadProof = [&](auto makeContextHash) {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            ConfidentialEnv confEnv{
+                env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
+            auto& mptAlice = confEnv.mpt;
+
+            std::uint64_t const amt = 10;
+            Buffer const blindingFactor = generateBlindingFactor();
+            Buffer const pcBlindingFactor = generateBlindingFactor();
+
+            auto const spendingBalance =
+                mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+            auto const encryptedSpendingBalance =
+                mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
+            if (!BEAST_EXPECT(spendingBalance && encryptedSpendingBalance))
+                return;
+
+            Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(
+                requireOptional(spendingBalance, "Missing spending balance"), pcBlindingFactor);
+            Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
+            Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
+            auto const version = mptAlice.getMPTokenVersion(bob);
+
+            Buffer const proof = mptAlice.getConvertBackProof(
+                bob,
+                amt,
+                makeContextHash(env, mptAlice, alice, bob, carol, version),
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = requireOptional(spendingBalance, "Missing spending balance"),
+                    .encryptedAmt = requireOptionalRef(
+                        encryptedSpendingBalance, "Missing encrypted spending balance"),
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            mptAlice.convertBack({
+                .account = bob,
+                .amt = amt,
+                .proof = proof,
+                .holderEncryptedAmt = bobCiphertext,
+                .issuerEncryptedAmt = issuerCiphertext,
+                .blindingFactor = blindingFactor,
+                .pedersenCommitment = pedersenCommitment,
+                .err = tecBAD_PROOF,
+            });
+        };
+
+        // Wrong account in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const& carol,
+                        std::uint32_t version) {
+            return getConvertBackContextHash(carol.id(), mpt.issuanceID(), env.seq(bob), version);
+        });
+
+        // Wrong issuance ID in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const&,
+                        Account const& alice,
+                        Account const& bob,
+                        Account const&,
+                        std::uint32_t version) {
+            return getConvertBackContextHash(
+                bob.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(bob), version);
+        });
+
+        // Wrong transaction sequence in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const&,
+                        std::uint32_t version) {
+            return getConvertBackContextHash(bob.id(), mpt.issuanceID(), env.seq(bob) + 1, version);
+        });
+
+        // Wrong balance version in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const&,
+                        std::uint32_t version) {
+            return getConvertBackContextHash(bob.id(), mpt.issuanceID(), env.seq(bob), version + 1);
+        });
+    }
+
+    // This test simulates a valid proof π extracted from a transaction
+    // for amount m1 is reused in a new transaction for a different
+    // amount m2 with different ciphertexts. It confirms the context hash
+    // recomputation fails due to the ciphertext binding mismatch, resulting
+    // in tecBAD_PROOF.
+    void
+    testConvertBackProofCiphertextBinding(FeatureBitset features)
+    {
+        testcase("ConvertBack: proof ciphertext binding");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob");
+        ConfidentialEnv confEnv{
+            env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        auto const spendingBalance = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing spending balance");
+        auto const encryptedSpendingBalance = requireOptional(
+            mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing encrypted spending balance");
+        auto const version = mptAlice.getMPTokenVersion(bob);
+        Buffer const pcBlindingFactor = generateBlindingFactor();
+        Buffer const pedersenCommitment =
+            mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
+
+        // Generate a valid proof pi for Amount m1 = 10
+        uint64_t const amtA = 10;
+        uint32_t const currentSeq = env.seq(bob);
+        uint256 const contextHashA =
+            getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, version);
+
+        Buffer const proofA = mptAlice.getConvertBackProof(
+            bob,
+            amtA,
+            contextHashA,
+            {
+                .pedersenCommitment = pedersenCommitment,
+                .amt = spendingBalance,
+                .encryptedAmt = encryptedSpendingBalance,
+                .blindingFactor = pcBlindingFactor,
+            });
+
+        // Construct Transaction B with Amount m2 = 20 and attach Proof pi
+        uint64_t const amtB = 20;
+        Buffer const blindingFactorB = generateBlindingFactor();
+        Buffer const bobCiphertextB = mptAlice.encryptAmount(bob, amtB, blindingFactorB);
+        Buffer const issuerCiphertextB = mptAlice.encryptAmount(alice, amtB, blindingFactorB);
+
+        // We attempt to verify the proof pi (for amt 10) against the new ciphertexts (for amt 20).
+        mptAlice.convertBack({
+            .account = bob,
+            .amt = amtB,
+            .proof = proofA,  // Extracted/Reused proof from Transaction A
+            .holderEncryptedAmt = bobCiphertextB,
+            .issuerEncryptedAmt = issuerCiphertextB,
+            .blindingFactor = blindingFactorB,
+            .pedersenCommitment = pedersenCommitment,
+            .err = tecBAD_PROOF,  // Expected failure
+        });
+    }
+
+    // This test simulates a valid proof π and ciphertext are
+    // tied to version v, but are reused after an inbox merge has incremented
+    // the CBS version to v+1. It confirms the validator rejects the transaction
+    // before acceptance due to the ContextID mismatch.
+    void
+    testConvertBackProofVersionMismatch(FeatureBitset features)
+    {
+        testcase("ConvertBack: proof version mismatch");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob");
+        ConfidentialEnv confEnv{
+            env, alice, {{.account = bob, .payAmount = 1000, .convertAmount = 100}}};
+        auto& mptAlice = confEnv.mpt;
+
+        auto const versionV = mptAlice.getMPTokenVersion(bob);
+        auto const spendingBalanceV = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing spending balance");
+        auto const encryptedSpendingBalanceV = requireOptional(
+            mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing encrypted spending balance");
+
+        // Parameters for the intended ConvertBack transaction
+        uint64_t const amt = 10;
+        Buffer const blindingFactor = generateBlindingFactor();
+        Buffer const pcBlindingFactor = generateBlindingFactor();
+        Buffer const pedersenCommitment =
+            mptAlice.getPedersenCommitment(spendingBalanceV, pcBlindingFactor);
+        Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
+        Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
+
+        // State Change: Increment version to v+1
+        // Converting more funds and merging increments the sfConfidentialBalanceVersion
+        mptAlice.convert({
+            .account = bob,
+            .amt = 50,
+        });
+        mptAlice.mergeInbox({
+            .account = bob,
+        });
+
+        BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) > versionV);
+
+        // Attack: Attempt to reuse proof tied to Version v at ledger Version v+1
+        uint32_t const currentSeq = env.seq(bob);
+        // Proof is explicitly generated using the outdated Version v
+        uint256 const oldContextHash =
+            getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, versionV);
+
+        Buffer const oldProof = mptAlice.getConvertBackProof(
+            bob,
+            amt,
+            oldContextHash,
+            {
+                .pedersenCommitment = pedersenCommitment,
+                .amt = spendingBalanceV,
+                .encryptedAmt = encryptedSpendingBalanceV,
+                .blindingFactor = pcBlindingFactor,
+            });
+
+        // Submit and verify failure
+        mptAlice.convertBack({
+            .account = bob,
+            .amt = amt,
+            .proof = oldProof,
+            .holderEncryptedAmt = bobCiphertext,
+            .issuerEncryptedAmt = issuerCiphertext,
+            .blindingFactor = blindingFactor,
+            .pedersenCommitment = pedersenCommitment,
+            .err = tecBAD_PROOF,  // Fails because TransactionContextID differs
+        });
+    }
+
+    /* This test simulates an attack where the holder ciphertext is modified
+     * via homomorphic addition (adding Encrypted_amt(1)) while leaving the issuer
+     * ciphertext unchanged. It confirms that the validator detects the
+     * mismatch between the re-computed ciphertexts and the submitted ones,
+     * resulting in tecBAD_PROOF.   */
+    void
+    testConvertBackHomomorphicCiphertextModification(FeatureBitset features)
+    {
+        testcase("ConvertBack: homomorphic ciphertext modification");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob");
+        ConfidentialEnv confEnv{
+            env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // Prepare valid parameters for a ConvertBack of 10
+        uint64_t const amt = 10;
+        Buffer const bf = generateBlindingFactor();
+
+        auto const holderCipherText = mptAlice.encryptAmount(bob, amt, bf);
+        auto const issuerCipherText = mptAlice.encryptAmount(alice, amt, bf);
+
+        // Generate a "Delta" ciphertext (Encrypting 1)
+        // We use Bob's key because we are tampering with Bob's (Holder's) field
+        Buffer const deltaBf = generateBlindingFactor();
+        auto const deltaCipherText = mptAlice.encryptAmount(bob, 1, deltaBf);
+
+        // Homomorphically add Delta to HolderCipherText: Tampered = Enc(10) + Enc(1) = Enc(11)
+        Buffer tamperedHolderCipherText = requireOptional(
+            homomorphicAdd(holderCipherText, deltaCipherText), "Missing tampered ciphertext");
+
+        // Generate a valid proof for the ORIGINAL amount (10)
+        auto const spendingBal = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing spending balance");
+        auto const spendingBalEnc = requireOptional(
+            mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing encrypted spending balance");
+        Buffer const pcBf = generateBlindingFactor();
+        auto const pedersenCommitment = mptAlice.getPedersenCommitment(spendingBal, pcBf);
+
+        auto const currentVersion = mptAlice.getMPTokenVersion(bob);
+        // Uses the new signature: Account, IssuanceID, Sequence, Version
+        uint256 const contextHash =
+            getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
+
+        Buffer const proof = mptAlice.getConvertBackProof(
+            bob,
+            amt,
+            contextHash,
+            {
+                .pedersenCommitment = pedersenCommitment,
+                .amt = spendingBal,
+                .encryptedAmt = spendingBalEnc,
+                .blindingFactor = pcBf,
+            });
+
+        // Submit transaction with Divergent Ciphertexts
+        // Holder Ciphertext encrypts 11. Issuer Ciphertext encrypts 10.
+        // The consistency check (re-encryption of `amt` with `bf`) will match Issuer but FAIL for
+        // Holder.
+        mptAlice.convertBack({
+            .account = bob,
+            .amt = amt,
+            .proof = proof,
+            .holderEncryptedAmt = tamperedHolderCipherText,  // Tampered (11)
+            .issuerEncryptedAmt = issuerCipherText,          // Original (10)
+            .blindingFactor = bf,
+            .pedersenCommitment = pedersenCommitment,
+            .err = tecBAD_PROOF,
+        });
+    }
+
+    /* This test verifies that xrpld correctly rejects attempts to
+     * overflow the maximum allowable token amount via homomorphic manipulation.
+     * It simulates an attack where an individual takes a valid ciphertext encrypting
+     * the maximum amount (kMaxMpTokenAmount) and homomorphically adds an encryption of
+     * 1 to it, producing a ciphertext for MAX+1. The test confirms that the Bulletproof
+     * range proof or inner-product constraints detect this overflow and invalidate the
+     * transaction, preserving the supply invariant. */
+    void
+    testSendHomomorphicOverflow(FeatureBitset features)
+    {
+        testcase("Send: homomorphic overflow attack via Enc(MAX) + Enc(1)");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 100},
+             {.account = carol, .payAmount = 50, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // Bob sends 10 to carol.  The send amount (10) and Bob's remaining balance
+        // (90) are both within [0, kMaxMpTokenAmount].  Range proof passes.
+        mptAlice.send({.account = bob, .dest = carol, .amt = 10});
+
+        // Bob's spending balance is 90 after the baseline send.
+        auto const bobSpendingBefore =
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+        BEAST_EXPECT(bobSpendingBefore == 90);
+
+        // Construct Enc(kMaxMpTokenAmount) with Bob's public key.
+        Buffer const bf1 = generateBlindingFactor();
+        Buffer const encMax = mptAlice.encryptAmount(bob, kMaxMpTokenAmount, bf1);
+
+        // Construct Enc(1) with a separate blinding factor.
+        Buffer const bf2 = generateBlindingFactor();
+        Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2);
+
+        // Homomorphically add to produce CB_S_holder' = Enc(MAX) + Enc(1)
+        Buffer overflowedCt =
+            requireOptional(homomorphicAdd(encMax, encOne), "Missing overflowed ciphertext");
+
+        // Submit the send transaction with the tampered ciphertext.
+        // Setting amt = kMaxMpTokenAmount + 1 drives proof generation for the
+        // overflowed value.  The bulletproof range check [0, kMaxMpTokenAmount]
+        // rejects MAX+1; the validator must return tecBAD_PROOF.
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = kMaxMpTokenAmount + 1,
+            .senderEncryptedAmt = overflowedCt,
+            .err = tecBAD_PROOF,
+        });
+
+        auto const bobSpendingAfter =
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+        BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter);
+    }
+
+    /* This test ensures that the system prevents underflow attacks where a user
+     * attempts to create a negative balance through homomorphic subtraction. It
+     * simulates a scenario where an attacker takes a ciphertext encrypting zero
+     * and subtracts an encryption of 1, resulting in a value of -1.
+     * The test asserts that the range proof verification fails because the resulting
+     * value falls outside the valid non-negative range [0, kMaxMpTokenAmount],
+     * causing the validator to reject the transaction with tecBAD_PROOF. */
+    void
+    testConvertBackHomomorphicUnderflow(FeatureBitset features)
+    {
+        testcase("ConvertBack: homomorphic underflow attack via Enc(0) - Enc(1)");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob");
+        ConfidentialEnv confEnv{
+            env, alice, {{.account = bob, .payAmount = 10, .convertAmount = 10}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // Converting back 1 from 10 leaves remaining balance = 9 (non-negative).
+        // Range proof [0, kMaxMpTokenAmount] passes.
+        mptAlice.convertBack({.account = bob, .amt = 1});
+
+        // Bob's spending balance is now 9; public balance is 1.
+        auto const bobSpendingBefore =
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+        BEAST_EXPECT(bobSpendingBefore == 9);
+        auto const bobPublicBefore = mptAlice.getBalance(bob);
+        BEAST_EXPECT(bobPublicBefore == 1);
+
+        // Construct Enc(0) — the zero encrypted balance using Bob's key.
+        Buffer const bf1 = generateBlindingFactor();
+        Buffer const encZero = mptAlice.encryptAmount(bob, 0, bf1);
+
+        // Construct Enc(1) with a separate blinding factor.
+        Buffer const bf2 = generateBlindingFactor();
+        Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2);
+
+        // Homomorphically subtract to produce CB_S_holder' = Enc(0) − Enc(1)
+        // = Enc(−1), which lies below [0, kMaxMpTokenAmount].
+        Buffer underflowedCt =
+            requireOptional(homomorphicSubtract(encZero, encOne), "Missing underflowed ciphertext");
+
+        // The underflowed value as uint64_t: 0 - 1 wraps to 0xFFFFFFFFFFFFFFFF.
+        // Generate a real proof using this wrapped value. The validator must still reject it
+        // because 0xFFFFFFFFFFFFFFFE (remaining balance) is outside [0, kMaxMpTokenAmount].
+        constexpr std::uint64_t kUnderflowedAmt =
+            static_cast(0) - static_cast(1);
+
+        Buffer const pcBf = generateBlindingFactor();
+        Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(kUnderflowedAmt, pcBf);
+
+        auto const currentVersion = mptAlice.getMPTokenVersion(bob);
+        uint256 const contextHash =
+            getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
+
+        Buffer const proof = mptAlice.getConvertBackProof(
+            bob,
+            1,
+            contextHash,
+            {
+                .pedersenCommitment = pedersenCommitment,
+                .amt = kUnderflowedAmt,
+                .encryptedAmt = underflowedCt,
+                .blindingFactor = pcBf,
+            });
+
+        mptAlice.convertBack({
+            .account = bob,
+            .amt = 1,
+            .proof = proof,
+            .holderEncryptedAmt = underflowedCt,
+            .pedersenCommitment = pedersenCommitment,
+            .err = tecBAD_PROOF,
+        });
+
+        // Supply invariant: both public and confidential balances must be unchanged
+        // after the rejected attack.
+        BEAST_EXPECT(mptAlice.getBalance(bob) == bobPublicBefore);
+        auto const bobSpendingAfter =
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+        BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter);
+    }
+
+    // Confidential sends carry encrypted amounts and a zero-knowledge proof.
+    // Both are built from elliptic-curve math, so every coordinate in the
+    // transaction must be a real point on the secp256k1 curve. These three
+    // variants confirm the validator rejects garbage coordinates at the right
+    // stage before any expensive cryptographic verification runs.
+    void
+    testSendInvalidCurvePoints(FeatureBitset features)
+    {
+        testcase("Send: off-curve EC points");
+        using namespace test::jtx;
+
+        // Variant A: garbage coordinate in ciphertext / commitment fields
+        // getBadCiphertext() looks structurally valid (correct length, right
+        // prefix byte 0x02) but its x-coordinate is 0xFF...FF, which does not
+        // lie on secp256k1. Preflight must reject before any ledger access.
+        {
+            Account const alice("alice"), bob("bob"), carol("carol");
+            Env env{*this, features};
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60},
+                 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
+            auto& mptAlice = confEnv.mpt;
+
+            // sender's encrypted amount has an invalid coordinate
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = getBadCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // recipient's encrypted amount has an invalid coordinate
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .destEncryptedAmt = getBadCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // issuer's encrypted amount has an invalid coordinate
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .issuerEncryptedAmt = getBadCiphertext(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // The amount and balance commitments are single curve coordinates
+            // used to tie the proof to the transfer amount and sender balance.
+            // A commitment with a valid-looking prefix but an impossible
+            // x-coordinate must also be rejected.
+            Buffer badCommitment(kEcPedersenCommitmentLength);
+            std::memset(badCommitment.data(), 0xFF, kEcPedersenCommitmentLength);
+            badCommitment.data()[0] = kEcCompressedPrefixEvenY;
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .amountCommitment = badCommitment,
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temMALFORMED,
+            });
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = badCommitment,
+                .err = temMALFORMED,
+            });
+        }
+
+        // Variant B: garbage coordinates inside the ZKP proof blob
+        // The proof blob has the right total byte length (so it passes the
+        // length check at preflight), but every embedded coordinate is
+        // 0xFF...FF — impossible on secp256k1. The proof verifier must detect
+        // this and return tecBAD_PROOF without crashing.
+        {
+            Account const alice("alice"), bob("bob"), carol("carol");
+            Env env{*this, features};
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60},
+                 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
+            auto& mptAlice = confEnv.mpt;
+
+            Buffer badProof(kEcSendProofLength);
+            std::memset(badProof.data(), 0xFF, kEcSendProofLength);
+            badProof.data()[0] = kEcCompressedPrefixEvenY;
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = strHex(badProof),
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // Variant C: only one of the two ciphertext coordinates is bad
+        // Each encrypted amount is two coordinates back-to-back: C1 then C2.
+        // Both must be valid. These tests corrupt only one at a time to
+        // confirm both are checked independently.
+        {
+            Account const alice("alice"), bob("bob"), carol("carol");
+            Env env{*this, features};
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 60},
+                 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
+            auto& mptAlice = confEnv.mpt;
+
+            // getTrivialCiphertext() has both C1 and C2 as valid (but trivial)
+            // curve coordinates.  We replace one half at a time with 0xFF...FF.
+            auto const& tc = getTrivialCiphertext();
+
+            // C1 = bad (0xFF...FF), C2 = valid trivial point
+            Buffer badC1goodC2(kEcGamalEncryptedTotalLength);
+            std::memset(badC1goodC2.data(), 0xFF, kEcGamalEncryptedTotalLength);
+            badC1goodC2.data()[0] = kEcCompressedPrefixEvenY;
+            std::memcpy(
+                badC1goodC2.data() + kEcCiphertextComponentLength,
+                tc.data() + kEcCiphertextComponentLength,
+                kEcCiphertextComponentLength);
+
+            // C1 = valid trivial point, C2 = bad (0xFF...FF)
+            Buffer goodC1badC2(kEcGamalEncryptedTotalLength);
+            std::memset(goodC1badC2.data(), 0xFF, kEcGamalEncryptedTotalLength);
+            std::memcpy(goodC1badC2.data(), tc.data(), kEcCiphertextComponentLength);
+            goodC1badC2.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
+
+            // sender's encrypted amount — bad C1
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = badC1goodC2,
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // sender's encrypted amount — bad C2
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .senderEncryptedAmt = goodC1badC2,
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // recipient's encrypted amount — bad C1
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .destEncryptedAmt = badC1goodC2,
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+
+            // recipient's encrypted amount — bad C2
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .proof = getTrivialSendProofHex(),
+                .destEncryptedAmt = goodC1badC2,
+                .amountCommitment = getTrivialCommitment(),
+                .balanceCommitment = getTrivialCommitment(),
+                .err = temBAD_CIPHERTEXT,
+            });
+        }
+    }
+
+    // Reject points from the wrong elliptic curve (wrong-group injection).
+    //
+    // An attacker might submit coordinates that come from a completely
+    // different elliptic curve, for example, the one used in TLS
+    // certificates (NIST P-256). If those coordinates happen to also be
+    // valid points on secp256k1 (which is possible since both curves use
+    // 256-bit fields), the format check at preflight will pass. However,
+    // the zero-knowledge proof is built specifically for secp256k1: the
+    // math inside the proof only holds for the right curve, so any
+    // transaction carrying cross-curve data will still be rejected at
+    // proof verification (tecBAD_PROOF).
+    void
+    testSendWrongGroupPointInjection(FeatureBitset features)
+    {
+        testcase("Send: wrong-group point injection rejected");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 60},
+             {.account = carol, .payAmount = 50, .convertAmount = 30}}};
+        auto& mptAlice = confEnv.mpt;
+
+        // The x-coordinate of the NIST P-256 generator point — a real,
+        // well-known value from a different elliptic curve (used in TLS
+        // and certificates).  This x-coordinate is also a valid secp256k1
+        // point, so it passes preflight.  Rejection happens at proof
+        // verification because the ZKP is secp256k1-specific.
+        //
+        //   P-256 generator x:
+        //     6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
+        static constexpr std::uint8_t kP256GeneratorX[32] = {
+            0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, 0xF8, 0xBC, 0xE6,
+            0xE5, 0x63, 0xA4, 0x40, 0xF2, 0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB,
+            0x33, 0xA0, 0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96,
+        };
+
+        // A 66-byte encrypted amount using the P-256 x-coordinate for both halves.
+        Buffer wrongGroupCt(kEcGamalEncryptedTotalLength);
+        wrongGroupCt.data()[0] = kEcCompressedPrefixEvenY;
+        std::memcpy(wrongGroupCt.data() + 1, kP256GeneratorX, 32);
+        wrongGroupCt.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
+        std::memcpy(wrongGroupCt.data() + kEcCiphertextComponentLength + 1, kP256GeneratorX, 32);
+
+        // A 33-byte commitment using the same wrong-curve x-coordinate.
+        Buffer wrongGroupCommitment(kEcPedersenCommitmentLength);
+        wrongGroupCommitment.data()[0] = kEcCompressedPrefixEvenY;
+        std::memcpy(wrongGroupCommitment.data() + 1, kP256GeneratorX, 32);
+
+        // sender's encrypted amount uses a coordinate from the wrong curve
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+            .proof = getTrivialSendProofHex(),
+            .senderEncryptedAmt = wrongGroupCt,
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecBAD_PROOF,
+        });
+
+        // recipient's encrypted amount uses a coordinate from the wrong curve
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+            .proof = getTrivialSendProofHex(),
+            .destEncryptedAmt = wrongGroupCt,
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecBAD_PROOF,
+        });
+
+        // issuer's encrypted amount uses a coordinate from the wrong curve
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+            .proof = getTrivialSendProofHex(),
+            .issuerEncryptedAmt = wrongGroupCt,
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecBAD_PROOF,
+        });
+
+        // amount commitment uses a coordinate from the wrong curve
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+            .proof = getTrivialSendProofHex(),
+            .amountCommitment = wrongGroupCommitment,
+            .balanceCommitment = getTrivialCommitment(),
+            .err = tecBAD_PROOF,
+        });
+
+        // balance commitment uses a coordinate from the wrong curve
+        mptAlice.send({
+            .account = bob,
+            .dest = carol,
+            .amt = 10,
+            .proof = getTrivialSendProofHex(),
+            .amountCommitment = getTrivialCommitment(),
+            .balanceCommitment = wrongGroupCommitment,
+            .err = tecBAD_PROOF,
+        });
+    }
+
+    // Reject an all-zero "null" public key.
+    //
+    // Every account in a confidential transfer needs a real public key —
+    // a specific point on the secp256k1 curve derived from a secret number
+    // only that account knows. An all-zero key (33 bytes of 0x00) is not
+    // a real key. It has no secret behind it, and encrypting data to it
+    // would not actually hide anything. The validator must reject it at
+    // preflight so no account can ever register a broken key.
+    void
+    testConvertIdentityElementRejection(FeatureBitset features)
+    {
+        testcase("Convert: all-zero public key rejected");
+        using namespace test::jtx;
+
+        // 33 zero bytes — not a real public key; no valid secret maps to this.
+        Buffer const nullKey = gMakeZeroBuffer(kEcPubKeyLength);
+
+        // Recipient (holder) tries to register an all-zero key.
+        // Must be rejected so no account ends up with an unprotected balance.
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob"), carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({.account = bob});
+            mptAlice.authorize({.account = carol});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 50);
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            // recipient (carol) tries to register an all-zero key
+            mptAlice.convert({
+                .account = carol,
+                .amt = 10,
+                .holderPubKey = nullKey,
+                .err = temMALFORMED,
+            });
+
+            // sender (bob) tries to register an all-zero key
+            mptAlice.convert({
+                .account = bob,
+                .amt = 10,
+                .holderPubKey = nullKey,
+                .err = temMALFORMED,
+            });
+        }
+
+        // Issuer tries to register an all-zero key.
+        // The issuer's key is used to encrypt the issuer's copy of every
+        // transfer amount.
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob");
+            MPTTester mptAlice(env, alice, {.holders = {bob}});
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+            mptAlice.authorize({.account = bob});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = nullKey,
+                .err = temMALFORMED,
+            });
+        }
+    }
+
+    /* This test ensures that when sending confidential tokens, the encrypted
+     * amounts are securely locked to the correct accounts' official public keys.
+     *
+     * Attack scenario — Encrypting the issuer's copy with the wrong key:
+     * A sender correctly encrypts the hidden transfer amount for themselves
+     * and the receiver. However, they intentionally encrypt the issuer's
+     * copy of the data using the wrong public key (for example, using the
+     * receiver's key instead of the official issuer's key). */
+    void
+    testSendWrongIssuerPublicKey(FeatureBitset features)
+    {
+        testcase("Send: issuer ciphertext encrypted under wrong public key");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 100},
+             {.account = carol, .payAmount = 50, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        auto const bobSpendingBefore =
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+
+        // issuer ciphertext encrypted under carol's holder key
+        // (should be under alice's registered issuer key).
+        {
+            Buffer const bf = generateBlindingFactor();
+            Buffer const wrongIssuerCt = mptAlice.encryptAmount(carol, 10, bf);
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .issuerEncryptedAmt = wrongIssuerCt,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // issuer ciphertext encrypted under bob's holder key
+        // (the sender's own key — still not the registered issuer key).
+        {
+            Buffer const bf = generateBlindingFactor();
+            Buffer const wrongIssuerCt = mptAlice.encryptAmount(bob, 10, bf);
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = 10,
+                .issuerEncryptedAmt = wrongIssuerCt,
+                .err = tecBAD_PROOF,
+            });
+        }
+
+        // all balances unchanged
+        BEAST_EXPECT(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
+            bobSpendingBefore);
+        BEAST_EXPECT(mptAlice.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+    }
+
+    // This test verifies that the compact AND-composed Send sigma proof
+    // enforces the shared-randomness invariant across participants.
+    void
+    testSendSharedRandomnessViolation(FeatureBitset features)
+    {
+        testcase("divergent C1 across participants in ConfidentialMPTSend");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const auditor("auditor");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 100, .convertAmount = 50},
+             {.account = carol, .payAmount = 50, .convertAmount = 50}},
+            tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
+            auditor};
+        auto& mptAlice = confEnv.mpt;
+
+        // Send amount is 10.
+        uint64_t const amt = 10;
+
+        enum class Participant { Sender, Dest, Issuer, Auditor };
+
+        // This lambda submits a send transaction where one of the four ciphertexts
+        // is encrypted with different randomness than the one used to build the proof.
+        // Note: When divergent is nullopt, all participants
+        // will use the same randomness and expected to succeed, this is the
+        // control case that confirms the test setup itself is sound, the bad proof
+        // is actually from divergent randomness, not other causes.
+        auto submitWithDivergentC1 = [&](std::optional divergent) {
+            ConfidentialSendSetup setup(mptAlice, bob, carol, alice, amt, std::cref(auditor));
+
+            auto const proofOpt =
+                requireOptional(setup.generateProof(mptAlice, env, bob, carol), "Missing proof");
+
+            // Re-encrypt one participant's ciphertext with divergent randomness.
+            Buffer senderCt = setup.senderAmt;
+            Buffer destCt = setup.destAmt;
+            Buffer issuerCt = setup.issuerAmt;
+            Buffer auditorCt =
+                requireOptionalRef(setup.auditorAmt, "Missing auditor encrypted amount");
+            if (divergent)
+            {
+                Buffer const bfDivergent = generateBlindingFactor();
+                switch (*divergent)
+                {
+                    case Participant::Sender:
+                        senderCt = mptAlice.encryptAmount(bob, amt, bfDivergent);
+                        break;
+                    case Participant::Dest:
+                        destCt = mptAlice.encryptAmount(carol, amt, bfDivergent);
+                        break;
+                    case Participant::Issuer:
+                        issuerCt = mptAlice.encryptAmount(alice, amt, bfDivergent);
+                        break;
+                    case Participant::Auditor:
+                        auditorCt = mptAlice.encryptAmount(auditor, amt, bfDivergent);
+                        break;
+                }
+            }
+
+            TER const expectedErr = divergent ? TER{tecBAD_PROOF} : TER{tesSUCCESS};
+
+            mptAlice.send({
+                .account = bob,
+                .dest = carol,
+                .amt = amt,
+                .proof = strHex(proofOpt),
+                .senderEncryptedAmt = senderCt,
+                .destEncryptedAmt = destCt,
+                .issuerEncryptedAmt = issuerCt,
+                .auditorEncryptedAmt = auditorCt,
+                .blindingFactor = setup.blindingFactor,
+                .amountCommitment = setup.amountCommitment,
+                .balanceCommitment = setup.balanceCommitment,
+                .err = expectedErr,
+            });
+
+            // Verify balances.
+            auto const spendingAfter =
+                mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+            if (divergent)
+            {
+                BEAST_EXPECT(spendingAfter == setup.prevSpending);
+            }
+            else
+            {
+                BEAST_EXPECT(spendingAfter == setup.prevSpending - amt);
+            }
+        };
+
+        // This confirms the test setup is sound, if any of the divergent cases below
+        // fail, it is due to the C1 mismatch and not a setup bug.
+        submitWithDivergentC1(std::nullopt);
+
+        // Divergent C1 for different participants should all fail with tecBAD_PROOF:
+        submitWithDivergentC1(Participant::Sender);
+        submitWithDivergentC1(Participant::Dest);
+        submitWithDivergentC1(Participant::Issuer);
+        submitWithDivergentC1(Participant::Auditor);
+    }
+
+    void
+    testConfidentialMPTBaseFee(FeatureBitset features)
+    {
+        testcase("test confidential transactions fee");
+        using namespace test::jtx;
+
+        auto setup =
+            [&](MPTTester& mpt, Account const& alice, Account const& bob, Account const& carol) {
+                mpt.create({
+                    .ownerCount = 1,
+                    .flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer |
+                        tfMPTCanClawback,
+                });
+                mpt.authorize({.account = bob});
+                mpt.authorize({.account = carol});
+                mpt.pay(alice, bob, 100);
+                mpt.pay(alice, carol, 50);
+                mpt.generateKeyPair(alice);
+                mpt.generateKeyPair(bob);
+                mpt.generateKeyPair(carol);
+                mpt.set({.account = alice, .issuerPubKey = mpt.getPubKey(alice)});
+            };
+
+        // test expected base fee for confidential transactions
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob"), carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+            setup(mptAlice, alice, bob, carol);
+
+            auto const baseFee = env.current()->fees().base;
+            auto const expectedFee = baseFee * (kConfidentialFeeMultiplier + 1);
+
+            // lambda function to submit confidential transaction and check fee charged to the
+            // account
+            auto checkFee = [&](Account const& acct, auto&& submitFn) {
+                auto const before = env.balance(acct);
+                submitFn();
+                auto const after = env.balance(acct);
+                BEAST_EXPECT(before - after == expectedFee);
+            };
+
+            checkFee(bob, [&]() {
+                mptAlice.convert(
+                    {.account = bob,
+                     .amt = 50,
+                     .holderPubKey = mptAlice.getPubKey(bob),
+                     .fee = expectedFee});
+            });
+            checkFee(carol, [&]() {
+                mptAlice.convert(
+                    {.account = carol,
+                     .amt = 10,
+                     .holderPubKey = mptAlice.getPubKey(carol),
+                     .fee = expectedFee});
+            });
+            checkFee(bob, [&]() { mptAlice.mergeInbox({.account = bob, .fee = expectedFee}); });
+            checkFee(carol, [&]() { mptAlice.mergeInbox({.account = carol, .fee = expectedFee}); });
+            checkFee(bob, [&]() {
+                mptAlice.send({.account = bob, .dest = carol, .amt = 5, .fee = expectedFee});
+            });
+            checkFee(bob, [&]() {
+                mptAlice.convertBack({.account = bob, .amt = 5, .fee = expectedFee});
+            });
+            checkFee(alice, [&]() {
+                mptAlice.confidentialClaw(
+                    {.account = alice, .holder = carol, .amt = 15, .fee = expectedFee});
+            });
+        }
+
+        // test insufficient fee for confidential transactions
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob"), carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+            setup(mptAlice, alice, bob, carol);
+            auto const baseFee = env.current()->fees().base;
+            auto const expectedFee = baseFee * (kConfidentialFeeMultiplier + 1);
+
+            mptAlice.convert(
+                {.account = bob,
+                 .amt = 1,
+                 .holderPubKey = mptAlice.getPubKey(bob),
+                 .fee = expectedFee - 1,
+                 .err = telINSUF_FEE_P});
+            mptAlice.mergeInbox({.account = bob, .fee = baseFee, .err = telINSUF_FEE_P});
+            mptAlice.send(
+                {.account = bob,
+                 .dest = carol,
+                 .amt = 1,
+                 .fee = baseFee * kConfidentialFeeMultiplier,
+                 .err = telINSUF_FEE_P});
+            mptAlice.convertBack({.account = bob, .amt = 1, .fee = baseFee, .err = telINSUF_FEE_P});
+            mptAlice.confidentialClaw(
+                {.account = alice,
+                 .holder = carol,
+                 .amt = 1,
+                 .fee = baseFee,
+                 .err = telINSUF_FEE_P});
+        }
+
+        // test excessive fee for confidential transactions
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob"), carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+            setup(mptAlice, alice, bob, carol);
+
+            auto const baseFee = env.current()->fees().base;
+            auto const highFee = baseFee * (kConfidentialFeeMultiplier + 1) * 2;
+            auto const bobBefore = env.balance(bob);
+            mptAlice.convert(
+                {.account = bob,
+                 .amt = 1,
+                 .holderPubKey = mptAlice.getPubKey(bob),
+                 .fee = highFee});
+            BEAST_EXPECT(env.balance(bob) == bobBefore - highFee);
+        }
+    }
+
+    void
+    testSendForgedEqualityProof(FeatureBitset features)
+    {
+        testcase("Send: forged equality proof");
+
+        // Test that modifying a ciphertext after proof generation causes
+        // verification to fail. The Fiat-Shamir challenge binds ciphertexts
+        // to the proof, so any modification invalidates the proof.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
+
+        // Forge destination ciphertext (Enc(20) instead of Enc(10))
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer const forgedBlindingFactor = generateBlindingFactor();
+            auto const forgedDestAmt = mptAlice.encryptAmount(carol, 20, forgedBlindingFactor);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
+            args.destEncryptedAmt = forgedDestAmt;
+            mptAlice.send(args);
+        }
+
+        // Forge sender's ciphertext (Enc(5) instead of Enc(10))
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer const forgedBlindingFactor = generateBlindingFactor();
+            auto const forgedSenderAmt = mptAlice.encryptAmount(bob, 5, forgedBlindingFactor);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
+            args.senderEncryptedAmt = forgedSenderAmt;
+            mptAlice.send(args);
+        }
+
+        // Forge issuer's ciphertext (Enc(100) instead of Enc(10))
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer const forgedBlindingFactor = generateBlindingFactor();
+            auto const forgedIssuerAmt = mptAlice.encryptAmount(alice, 100, forgedBlindingFactor);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
+            args.issuerEncryptedAmt = forgedIssuerAmt;
+            mptAlice.send(args);
+        }
+    }
+
+    void
+    testSendForgedRangeProof(FeatureBitset features)
+    {
+        testcase("Send: forged range proof");
+
+        // Attack: send uint64_max tokens using Enc(uint64_max) ciphertexts
+        // and a corrupted bulletproof. Verifier rejects due to inner-product
+        // mismatch and Fiat-Shamir transcript divergence. Supply invariant
+        // is preserved.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const badAmount = std::numeric_limits::max();
+        Buffer const blindingFactor = generateBlindingFactor();
+
+        // Construct Enc(uint64_max) ciphertexts and commitment.
+        auto const senderAmt = mptAlice.encryptAmount(bob, badAmount, blindingFactor);
+        auto const destAmt = mptAlice.encryptAmount(carol, badAmount, blindingFactor);
+        auto const issuerAmt = mptAlice.encryptAmount(alice, badAmount, blindingFactor);
+        auto const amountCommitment = mptAlice.getPedersenCommitment(badAmount, blindingFactor);
+
+        // Balance commitment for Bob's actual balance.
+        auto const prevSpending = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing previous spending balance");
+        auto const balanceBlindingFactor = generateBlindingFactor();
+        auto const balanceCommitment =
+            mptAlice.getPedersenCommitment(prevSpending, balanceBlindingFactor);
+
+        // Generate a valid proof for a legitimate amount, then corrupt
+        // the bulletproof segment to simulate a forged range proof.
+        ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
+        auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
+        if (!BEAST_EXPECT(validProof.has_value()))
+            return;
+
+        // Corrupt bulletproof bytes.
+        Buffer forgedProof = requireOptional(validProof, "Missing valid proof");
+        for (size_t i = kBulletproofOffset; i < forgedProof.size(); i += 7)
+            forgedProof.data()[i] ^= 0xFF;
+
+        // Submit — rejected due to commitment mismatch.
+        mptAlice.send(
+            {.account = bob,
+             .dest = carol,
+             .amt = badAmount,
+             .proof = strHex(forgedProof),
+             .senderEncryptedAmt = senderAmt,
+             .destEncryptedAmt = destAmt,
+             .issuerEncryptedAmt = issuerAmt,
+             .amountCommitment = amountCommitment,
+             .balanceCommitment = balanceCommitment,
+             .err = tecBAD_PROOF});
+
+        // Supply invariant: Bob's balance unchanged.
+        auto const postSpending = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing post spending balance");
+        BEAST_EXPECT(postSpending == prevSpending);
+    }
+
+    void
+    testSendNegativeValueMalleability(FeatureBitset features)
+    {
+        testcase("Send: negative value malleability");
+
+        // Attack: forge a bulletproof claiming remaining = (uint64_t)(-10).
+        // Bob has 10 tokens, sends 10. Honest remaining is 0, but the
+        // forged proof claims 0xFFFFFFFFFFFFFFF6. Rejected because
+        // PC(0) != PC(0xFFFFFFFFFFFFFFF6).
+
+        using namespace test::jtx;
+        // Bob converts exactly 10 tokens, leaving honest remaining = 0.
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 1000, .convertAmount = 10},
+             {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+        uint64_t const negativeRemaining = static_cast(-10);  // 0xFFFFFFFFFFFFFFF6
+
+        ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+        auto const ctxHash = getSendContextHash(
+            bob.id(), mptAlice.issuanceID(), env.seq(bob), carol.id(), setup.version);
+
+        auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
+        if (!BEAST_EXPECT(validProof.has_value()))
+            return;
+
+        // Forge bulletproof for {10, 0xFFFFFFFFFFFFFFF6} and splice it in.
+        auto const forgedBulletproof = getForgedBulletproof(
+            {sendAmount, negativeRemaining},
+            {setup.amountBlindingFactor, setup.balanceBlindingFactor},
+            ctxHash);
+
+        Buffer forgedProof(requireOptionalRef(validProof, "Missing valid proof").size());
+        std::memcpy(
+            forgedProof.data(),
+            requireOptionalRef(validProof, "Missing valid proof").data(),
+            kBulletproofOffset);
+        std::memcpy(
+            forgedProof.data() + kBulletproofOffset,
+            forgedBulletproof.data(),
+            kEcDoubleBulletproofLength);
+
+        mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
+
+        // Supply invariant: Bob's balance unchanged.
+        auto const postSpending = requireOptional(
+            mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+            "Missing post spending balance");
+        BEAST_EXPECT(postSpending == setup.prevSpending);
+    }
+
+    void
+    testSendInvalidProofContextBinding(FeatureBitset features)
+    {
+        testcase("Send proof context binding");
+        using namespace test::jtx;
+
+        auto runBadProof = [&](auto makeContextHash) {
+            Env env{*this, features};
+            Account const alice("alice");
+            Account const bob("bob");
+            Account const carol("carol");
+            ConfidentialEnv confEnv{
+                env,
+                alice,
+                {{.account = bob, .payAmount = 100, .convertAmount = 40}, {.account = carol}}};
+            auto& mptAlice = confEnv.mpt;
+
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
+
+            auto const proof = mptAlice.getConfidentialSendProof(
+                bob,
+                setup.sendAmount,
+                setup.recipients,
+                setup.blindingFactor,
+                makeContextHash(env, mptAlice, alice, bob, carol, setup.version),
+                {
+                    .pedersenCommitment = setup.amountCommitment,
+                    .amt = setup.sendAmount,
+                    .encryptedAmt = setup.senderAmt,
+                    .blindingFactor = setup.amountBlindingFactor,
+                },
+                {
+                    .pedersenCommitment = setup.balanceCommitment,
+                    .amt = setup.prevSpending,
+                    .encryptedAmt = setup.prevEncryptedSpending,
+                    .blindingFactor = setup.balanceBlindingFactor,
+                });
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            mptAlice.send(setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF));
+        };
+
+        // Wrong sender account in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const& carol,
+                        std::uint32_t version) {
+            return getSendContextHash(
+                carol.id(), mpt.issuanceID(), env.seq(bob), carol.id(), version);
+        });
+
+        // Wrong issuance ID in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const&,
+                        Account const& alice,
+                        Account const& bob,
+                        Account const& carol,
+                        std::uint32_t version) {
+            return getSendContextHash(
+                bob.id(),
+                makeMptID(env.seq(alice) + 100, alice),
+                env.seq(bob),
+                carol.id(),
+                version);
+        });
+
+        // Wrong transaction sequence in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const& carol,
+                        std::uint32_t version) {
+            return getSendContextHash(
+                bob.id(), mpt.issuanceID(), env.seq(bob) + 1, carol.id(), version);
+        });
+
+        // Wrong destination in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const&,
+                        std::uint32_t version) {
+            return getSendContextHash(bob.id(), mpt.issuanceID(), env.seq(bob), bob.id(), version);
+        });
+
+        // Wrong balance version in the proof context.
+        runBadProof([&](Env& env,
+                        MPTTester const& mpt,
+                        Account const&,
+                        Account const& bob,
+                        Account const& carol,
+                        std::uint32_t version) {
+            return getSendContextHash(
+                bob.id(), mpt.issuanceID(), env.seq(bob), carol.id(), version + 1);
+        });
+    }
+
+    void
+    testSendFiatShamirBinding(FeatureBitset features)
+    {
+        testcase("Send: Fiat-Shamir Binding");
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
+
+        // Variant A: forged amount commitment.
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            auto const forgedBlindingFactor = generateBlindingFactor();
+            auto const forgedCommitment =
+                mptAlice.getPedersenCommitment(setup.sendAmount + 5, forgedBlindingFactor);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
+            args.amountCommitment = forgedCommitment;
+            mptAlice.send(args);
+        }
+
+        // Variant B: proof replay at a different sequence.
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            mptAlice.pay(bob, carol, 1);
+            env.close();
+
+            mptAlice.send(setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF));
+        }
+
+        // Variant C: tampered response scalars.
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            auto const& proofRef = requireOptionalRef(proof, "Missing proof");
+            Buffer tamperedProof(proofRef.size());
+            std::memcpy(tamperedProof.data(), proofRef.data(), proofRef.size());
+            size_t const tamperOffset = tamperedProof.size() / 2;
+            tamperedProof.data()[tamperOffset] ^= 0xFF;
+
+            mptAlice.send(setup.sendArgs(bob, carol, tamperedProof, tecBAD_PROOF));
+        }
+    }
+
+    void
+    testSendProofComponentReuse(FeatureBitset features)
+    {
+        testcase("Send: Proof Component Reuse");
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol"), dan("dan");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob},
+             {.account = carol, .payAmount = 1000, .convertAmount = 50},
+             {.account = dan, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+
+        // Variant A: replay proof to same destination after sequence changes.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            mptAlice.send(setup.sendArgs(bob, carol, requireOptionalRef(proof, "Missing proof")));
+            mptAlice.mergeInbox({.account = carol});
+
+            mptAlice.send(setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF));
+        }
+
+        // Variant B: replay proof to a different destination.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            mptAlice.send(setup.sendArgs(bob, carol, requireOptionalRef(proof, "Missing proof")));
+            mptAlice.mergeInbox({.account = carol});
+
+            auto const destAmtDan = mptAlice.encryptAmount(dan, sendAmount, setup.blindingFactor);
+            auto const issuerAmtDan =
+                mptAlice.encryptAmount(alice, sendAmount, setup.blindingFactor);
+
+            auto args =
+                setup.sendArgs(bob, dan, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
+            args.destEncryptedAmt = destAmtDan;
+            args.issuerEncryptedAmt = issuerAmtDan;
+            mptAlice.send(args);
+        }
+    }
+
+    void
+    testSendSpecialWitnessValues(FeatureBitset features)
+    {
+        testcase("Send: special witness values");
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
+        auto& mptAlice = confEnv.mpt;
+
+        ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
+
+        // Variant A: zero-valued response scalars.
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer forgedProof = requireOptionalRef(proof, "Missing proof");
+
+            static constexpr size_t kSigmaScalarSize = 32;
+            static constexpr size_t kChallengeOffset = 0;
+            static constexpr size_t kResponseOffset = kChallengeOffset + kSigmaScalarSize;
+            static constexpr size_t kResponseSize = 5 * kSigmaScalarSize;  // z_m..z_sk
+            std::memset(forgedProof.data() + kResponseOffset, 0, kResponseSize);
+
+            mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
+        }
+
+        // Variant B: identity element in ciphertext.
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer invalidCiphertext(kEcGamalEncryptedTotalLength);
+            std::memset(invalidCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), temBAD_CIPHERTEXT);
+            args.senderEncryptedAmt = invalidCiphertext;
+            mptAlice.send(args);
+        }
+
+        // Variant B2: identity element in commitment.
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer invalidCommitment(kEcPedersenCommitmentLength);
+            std::memset(invalidCommitment.data(), 0, kEcPedersenCommitmentLength);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(proof, "Missing proof"), temMALFORMED);
+            args.amountCommitment = invalidCommitment;
+            mptAlice.send(args);
+        }
+
+        // Variant C: boundary scalar (curve order).
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer forgedProof = requireOptionalRef(proof, "Missing proof");
+
+            static constexpr unsigned char kCurveOrder[32] = {
+                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,  //
+                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,  //
+                0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,  //
+                0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41   //
+            };
+
+            std::memcpy(forgedProof.data() + 32, kCurveOrder, 32);
+
+            mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
+        }
+
+        // Variant C2: overflow scalar (curve order + 1).
+        {
+            auto const proof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof.has_value()))
+                return;
+
+            Buffer forgedProof = requireOptionalRef(proof, "Missing proof");
+
+            static constexpr unsigned char kOverflowScalar[32] = {
+                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,  //
+                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,  //
+                0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,  //
+                0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x42   //
+            };
+
+            std::memcpy(forgedProof.data() + 32, kOverflowScalar, 32);
+
+            mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
+        }
+    }
+
+    void
+    testSendCrossStatementProofSubstitution(FeatureBitset features)
+    {
+        testcase("Send: cross-statement proof substitution");
+
+        // This test verifies that proofs generated for one protocol component
+        // cannot be used in place of another, and that proofs bound to
+        // different public parameters are rejected.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
+            tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer | tfMPTCanClawback};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+
+        // Variant A: Swap proof type (cross-statement substitution)
+        // -----------------------------------------------------------------
+        // Attack: Generate a valid convertBack proof (compact sigma +
+        // single bulletproof) and attempt to use it as the ZK proof in a
+        // ConfidentialMPTSend transaction.
+        //
+        // Expected: The send proof has a different structure
+        // (equality + 2×pedersen + double bulletproof). Even if sized to
+        // match, the domain-separated Fiat-Shamir transcript differs,
+        // so verification equations fail.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            // Generate a valid convertBack proof for bob
+            auto const spendingBalance = requireOptional(
+                mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
+                "Missing spending balance");
+            auto const encryptedSpending = requireOptional(
+                mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
+                "Missing encrypted spending balance");
+
+            Buffer const pcBlindingFactor = generateBlindingFactor();
+            Buffer const pedersenCommitment =
+                mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
+
+            auto const version = mptAlice.getMPTokenVersion(bob);
+            uint256 const convertBackCtxHash =
+                getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version);
+
+            Buffer const convertBackProof = mptAlice.getConvertBackProof(
+                bob,
+                sendAmount,
+                convertBackCtxHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = spendingBalance,
+                    .encryptedAmt = encryptedSpending,
+                    .blindingFactor = pcBlindingFactor,
+                });
+
+            // Resize the convertBack proof to match the expected send proof
+            // size so it passes preflight's size check and reaches the actual
+            // ZK verification in doApply.
+            auto const expectedSendSize = kEcSendProofLength;
+            Buffer resizedProof(expectedSendSize);
+            auto const copyLen = std::min(convertBackProof.size(), expectedSendSize);
+            std::memcpy(resizedProof.data(), convertBackProof.data(), copyLen);
+            // Zero-pad the rest (if convertBack proof is shorter)
+            if (copyLen < expectedSendSize)
+                std::memset(resizedProof.data() + copyLen, 0, expectedSendSize - copyLen);
+
+            mptAlice.send(setup.sendArgs(bob, carol, resizedProof, tecBAD_PROOF));
+        }
+
+        // Variant B: Valid proof bound to wrong public parameters
+        // -----------------------------------------------------------------
+        // Attack: Generate a valid send proof using a wrong context hash
+        // (computed with a different issuanceID). The proof is
+        // mathematically valid for the wrong statement, but when the
+        // verifier recomputes the Fiat-Shamir challenge using the correct
+        // issuanceID, the challenge differs and verification fails.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            // Compute context hash with a fabricated (wrong) issuanceID
+            uint192 const fakeIssuanceID{1};
+            auto const wrongCtxHash = getSendContextHash(
+                bob.id(), fakeIssuanceID, env.seq(bob), carol.id(), setup.version);
+
+            // Generate a proof that is valid for the wrong issuanceID
+            auto const wrongProof = mptAlice.getConfidentialSendProof(
+                bob,
+                sendAmount,
+                setup.recipients,
+                setup.blindingFactor,
+                wrongCtxHash,
+                {
+                    .pedersenCommitment = setup.amountCommitment,
+                    .amt = sendAmount,
+                    .encryptedAmt = setup.senderAmt,
+                    .blindingFactor = setup.amountBlindingFactor,
+                },
+                {
+                    .pedersenCommitment = setup.balanceCommitment,
+                    .amt = setup.prevSpending,
+                    .encryptedAmt = setup.prevEncryptedSpending,
+                    .blindingFactor = setup.balanceBlindingFactor,
+                });
+
+            if (!BEAST_EXPECT(wrongProof.has_value()))
+                return;
+
+            // Submit with the correct issuanceID — verifier recomputes
+            // the challenge using the real issuanceID, which differs from
+            // the one baked into the proof.
+            mptAlice.send(setup.sendArgs(
+                bob, carol, requireOptionalRef(wrongProof, "Missing wrong proof"), tecBAD_PROOF));
+        }
+    }
+
+    void
+    testSendCiphertextMalleability(FeatureBitset features)
+    {
+        testcase("Send: ciphertext malleability");
+
+        // Attack: replace ElGamal ciphertext Enc(m) with Enc(2m) to inflate
+        // the amount credited to the recipient. ElGamal is homomorphic, so
+        // scalar multiplication (C1, C2) → (k*C1, k*C2) decrypts to k*m.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
+            tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+
+        // Variant A: Post-signature tampering.
+        // Build a valid signed transaction, then replace the destination
+        // ciphertext with Enc(2m) in the serialized blob. The original
+        // signature no longer covers the modified data.
+        {
+            auto const seq = env.seq(bob);
+            auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
+            auto jtx = env.jt(jv);
+            BEAST_EXPECT(jtx.stx);
+
+            // Serialize signed tx, deserialize into mutable STObject
+            Serializer s;
+            jtx.stx->add(s);
+            SerialIter sit(s.slice());
+            STObject obj(sit, sfTransaction);
+
+            // Replace dest ciphertext with Enc(2m) — a valid EC point
+            // encrypting an inflated amount under carol's key
+            Buffer const bf = generateBlindingFactor();
+            auto const inflatedCiphertext = mptAlice.encryptAmount(carol, sendAmount * 2, bf);
+            obj.setFieldVL(sfDestinationEncryptedAmount, inflatedCiphertext);
+
+            // Re-serialize with the original (now-stale) signature
+            Serializer tampered;
+            obj.add(tampered);
+
+            // Signature verification fails — rejected before ZKP check
+            auto const jr = env.rpc("submit", strHex(tampered.slice()));
+            BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
+        }
+
+        // Variant B: Re-signed with inflated ciphertext.
+        // Generate a valid proof for amount m, then replace the destination
+        // ciphertext with Enc(2m) and re-sign. Signature passes, but the
+        // compact sigma proof fails: the proof binds Enc(m) to the Pedersen
+        // commitment PC(m, r), so substituting Enc(2m) breaks the linkage.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const ctxHash = getSendContextHash(
+                bob.id(), mptAlice.issuanceID(), env.seq(bob), carol.id(), setup.version);
+
+            auto const validProof = mptAlice.getConfidentialSendProof(
+                bob,
+                sendAmount,
+                setup.recipients,
+                setup.blindingFactor,
+                ctxHash,
+                {
+                    .pedersenCommitment = setup.amountCommitment,
+                    .amt = sendAmount,
+                    .encryptedAmt = setup.senderAmt,
+                    .blindingFactor = setup.amountBlindingFactor,
+                },
+                {
+                    .pedersenCommitment = setup.balanceCommitment,
+                    .amt = setup.prevSpending,
+                    .encryptedAmt = setup.prevEncryptedSpending,
+                    .blindingFactor = setup.balanceBlindingFactor,
+                });
+
+            if (!BEAST_EXPECT(validProof.has_value()))
+                return;
+
+            // Replace dest ciphertext with Enc(2m) using the same blinding
+            // factor — even with matching randomness the proof rejects
+            // because the committed plaintext differs
+            auto const inflatedDestAmt =
+                mptAlice.encryptAmount(carol, sendAmount * 2, setup.blindingFactor);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
+            args.destEncryptedAmt = inflatedDestAmt;
+            mptAlice.send(args);
+        }
+    }
+
+    void
+    testSendCiphertextNegation(FeatureBitset features)
+    {
+        testcase("Send: ciphertext negation");
+
+        // Attack: negate ciphertext -Enc(m) = (-C1, -C2) to reverse the
+        // transaction direction. Negation decrypts to the group-level
+        // additive inverse of m*G, effectively turning a credit into a debit.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
+            tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+
+        // Negate an ElGamal ciphertext by flipping the y-coordinate parity
+        // of both compressed EC points. For secp256k1 compressed form,
+        // prefix 0x02 means even-y and 0x03 means odd-y; negation
+        // swaps them: -P has the same x but opposite y.
+        auto negateCiphertext = [](Buffer const& ct) -> Buffer {
+            Buffer neg = ct;
+            neg.data()[0] ^= 0x01;                             // negate C1
+            neg.data()[kEcCiphertextComponentLength] ^= 0x01;  // negate C2
+            return neg;
+        };
+
+        // Variant A: Post-signature negation.
+        // Negate the destination ciphertext in the signed blob.
+        // Signature no longer covers the modified field.
+        {
+            auto const seq = env.seq(bob);
+            auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
+            auto jtx = env.jt(jv);
+            BEAST_EXPECT(jtx.stx);
+
+            Serializer s;
+            jtx.stx->add(s);
+
+            SerialIter sit(s.slice());
+            STObject obj(sit, sfTransaction);
+
+            auto const origDestAmt = obj.getFieldVL(sfDestinationEncryptedAmount);
+            Buffer const origBuf(origDestAmt.data(), origDestAmt.size());
+            auto const negDestAmt = negateCiphertext(origBuf);
+            obj.setFieldVL(
+                sfDestinationEncryptedAmount, Slice(negDestAmt.data(), negDestAmt.size()));
+
+            Serializer tampered;
+            obj.add(tampered);
+
+            auto const jr = env.rpc("submit", strHex(tampered.slice()));
+            BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
+        }
+
+        // Variant B: Re-signed with all negated ciphertexts.
+        // Signature passes, but the compact sigma proof fails — the proof
+        // was generated for Enc(m), not Enc(-m).
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(validProof.has_value()))
+                return;
+
+            // Negate all three ciphertexts: Enc(m) -> Enc(-m)
+            auto const negSenderAmt = negateCiphertext(setup.senderAmt);
+            auto const negDestAmt = negateCiphertext(setup.destAmt);
+            auto const negIssuerAmt = negateCiphertext(setup.issuerAmt);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
+            args.senderEncryptedAmt = negSenderAmt;
+            args.destEncryptedAmt = negDestAmt;
+            args.issuerEncryptedAmt = negIssuerAmt;
+            mptAlice.send(args);
+        }
+
+        // Variant C: Negate only the sender ciphertext.
+        // The verifier uses the sender ciphertext to derive the remainder
+        // commitment: Enc(b) - Enc(m) becomes Enc(b) - (-Enc(m)) = Enc(b+m).
+        // The bulletproof was generated for (b - m), not (b + m), so the
+        // aggregated range proof fails.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(validProof.has_value()))
+                return;
+
+            auto const negSenderAmt = negateCiphertext(setup.senderAmt);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
+            args.senderEncryptedAmt = negSenderAmt;
+            mptAlice.send(args);
+        }
+    }
+
+    void
+    testSendCiphertextCombination(FeatureBitset features)
+    {
+        testcase("Send: ciphertext combination");
+
+        // Attack: exploit ElGamal homomorphism to combine ciphertexts
+        // Enc(m1) + Enc(m2) = Enc(m1+m2), inflating the credited amount
+        // without knowing the private keys.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob, .payAmount = 1000, .convertAmount = 200},
+             {.account = carol, .payAmount = 1000, .convertAmount = 100}},
+            tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const m1 = 10;
+        uint64_t const m2 = 5;
+
+        // Variant A: Post-signature combination.
+        // Add Enc(m2) to the signed destination ciphertext Enc(m1).
+        // The original signature doesn't cover the combined ciphertext.
+        {
+            auto const seq = env.seq(bob);
+            auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = m1}, seq);
+            auto jtx = env.jt(jv);
+            BEAST_EXPECT(jtx.stx);
+
+            Serializer s;
+            jtx.stx->add(s);
+
+            SerialIter sit(s.slice());
+            STObject obj(sit, sfTransaction);
+
+            auto const origDestCt = obj.getFieldVL(sfDestinationEncryptedAmount);
+
+            // Homomorphically add Enc(m2) to the original Enc(m1)
+            Buffer const bf2 = generateBlindingFactor();
+            auto const encM2 = mptAlice.encryptAmount(carol, m2, bf2);
+            auto const combined = requireOptional(
+                homomorphicAdd(
+                    Slice(origDestCt.data(), origDestCt.size()), Slice(encM2.data(), encM2.size())),
+                "Missing combined ciphertext");
+
+            obj.setFieldVL(sfDestinationEncryptedAmount, combined);
+
+            Serializer tampered;
+            obj.add(tampered);
+
+            auto const jr = env.rpc("submit", strHex(tampered.slice()));
+            BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
+        }
+
+        // Variant B: Re-signed with combined ciphertext.
+        // Generate a valid proof for m1, then replace dest ciphertext with
+        // Enc(m1) + Enc(m2). Sigma proof fails because the proof was
+        // generated for Enc(m1) only — the combined ciphertext has
+        // different randomness.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, m1);
+
+            auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(validProof.has_value()))
+                return;
+
+            // Homomorphically add Enc(m2) to the valid dest ciphertext
+            Buffer const bf2 = generateBlindingFactor();
+            auto const encM2 = mptAlice.encryptAmount(carol, m2, bf2);
+            auto const combinedDest = homomorphicAdd(setup.destAmt, encM2);
+            BEAST_EXPECT(combinedDest.has_value());
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
+            args.destEncryptedAmt = combinedDest;
+            mptAlice.send(args);
+        }
+
+        // Variant C: Cross-transaction ciphertext reuse.
+        // Execute a valid send of m1, then build a new send for m2 using
+        // a combined ciphertext oldEnc(m1) + newEnc(m2) = Enc(m1+m2),
+        // where oldEnc(m1) is the actual ciphertext from the previous tx.
+        // The proof was generated for the new transaction's context, but
+        // the ciphertext includes stale randomness from the old Enc(m1).
+        {
+            // Execute a valid send of m1, capturing the actual ciphertext used
+            ConfidentialSendSetup const setup1(mptAlice, bob, carol, alice, m1);
+            auto const proof1 = setup1.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof1.has_value()))
+                return;
+            mptAlice.send(setup1.sendArgs(bob, carol, requireOptionalRef(proof1, "Missing proof")));
+
+            ConfidentialSendSetup const setup2(mptAlice, bob, carol, alice, m2);
+
+            auto const proof2 = setup2.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof2.has_value()))
+                return;
+
+            // Combine the actual prior-tx Enc(m1) with the new Enc(m2)
+            auto const crossCombined = homomorphicAdd(setup1.destAmt, setup2.destAmt);
+            BEAST_EXPECT(crossCombined.has_value());
+
+            auto args = setup2.sendArgs(
+                bob, carol, requireOptionalRef(proof2, "Missing proof"), tecBAD_PROOF);
+            args.destEncryptedAmt = crossCombined;
+            mptAlice.send(args);
+        }
+    }
+
+    void
+    testSendCiphertextRerandomization(FeatureBitset features)
+    {
+        testcase("Send: ciphertext rerandomization");
+
+        // Attack: substitute the randomness component C1 of an ElGamal
+        // ciphertext (C1, C2) while keeping the message component C2
+        // unchanged. This "rerandomizes" the ciphertext to break
+        // linkability or forge fresh-looking ciphertexts.
+        //
+        // The compact sigma proof binds C1 to the shared randomness used
+        // across all recipients, so any C1 substitution breaks the proof.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
+            tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+
+        // Helper: replace C1 in a ciphertext with C1 from another
+        // ciphertext, keeping C2 unchanged. Returns a rerandomized
+        // ciphertext (C1', C2).
+        auto substituteC1 = [](Buffer const& target, Buffer const& source) -> Buffer {
+            Buffer result = target;
+            // Copy C1 (the first ciphertext component) from source.
+            std::memcpy(result.data(), source.data(), kEcCiphertextComponentLength);
+            return result;
+        };
+
+        // Variant A: Post-signature C1 substitution.
+        // Replace C1 in the dest ciphertext after signing.
+        // Signature no longer covers the modified ciphertext.
+        {
+            auto const seq = env.seq(bob);
+            auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
+            auto jtx = env.jt(jv);
+            BEAST_EXPECT(jtx.stx);
+
+            Serializer s;
+            jtx.stx->add(s);
+            SerialIter sit(s.slice());
+            STObject obj(sit, sfTransaction);
+
+            // Generate a random C1' by encrypting a different amount
+            Buffer const bf2 = generateBlindingFactor();
+            auto const otherCt = mptAlice.encryptAmount(carol, 99, bf2);
+
+            // Replace C1 in the dest ciphertext
+            auto const origDestAmt = obj.getFieldVL(sfDestinationEncryptedAmount);
+            Buffer const origBuf(origDestAmt.data(), origDestAmt.size());
+            auto const rerandomized = substituteC1(origBuf, otherCt);
+            obj.setFieldVL(
+                sfDestinationEncryptedAmount, Slice(rerandomized.data(), rerandomized.size()));
+
+            Serializer tampered;
+            obj.add(tampered);
+
+            // Signature verification fails
+            auto const jr = env.rpc("submit", strHex(tampered.slice()));
+            BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
+        }
+
+        // Variant B: Re-signed C1 substitution.
+        // Replace C1 in the dest ciphertext with a fresh random point
+        // and re-sign. Sigma proof fails because the shared-randomness
+        // binding no longer holds — C1' wasn't generated with the same r
+        // used in the proof.
+        {
+            ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(validProof.has_value()))
+                return;
+
+            // Create a ciphertext with different randomness to get C1'
+            Buffer const bf2 = generateBlindingFactor();
+            auto const otherCt = mptAlice.encryptAmount(carol, sendAmount, bf2);
+
+            // Replace C1 in dest ciphertext, keep C2
+            auto const rerandomizedDest = substituteC1(setup.destAmt, otherCt);
+
+            auto args = setup.sendArgs(
+                bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
+            args.destEncryptedAmt = rerandomizedDest;
+            mptAlice.send(args);
+        }
+    }
+
+    void
+    testSendZeroRandomnessCiphertext(FeatureBitset features)
+    {
+        testcase("Send: zero randomness ciphertext");
+
+        // Setting r = 0 in ElGamal yields C1 = O (identity), C2 = mG —
+        // a deterministic ciphertext that reveals the plaintext.
+
+        using namespace test::jtx;
+        Env env{*this, features};
+        Account const alice("alice"), bob("bob"), carol("carol");
+        ConfidentialEnv confEnv{
+            env,
+            alice,
+            {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
+            tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
+        auto& mptAlice = confEnv.mpt;
+
+        uint64_t const sendAmount = 10;
+
+        // -----------------------------------------------------------------
+        // Variant A: Post-signature zero-randomness substitution
+        // -----------------------------------------------------------------
+        // Construct a valid ConfidentialMPTSend transaction with proper
+        // ciphertexts and ZKPs, sign it, then replace the sender ciphertext
+        // with a deterministic form (C1 = 0x00...00, C2 = arbitrary).
+        // Since the identity element has no valid compressed encoding,
+        // the modified blob fails deserialization / signature check.
+        {
+            auto const seq = env.seq(bob);
+            auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
+            auto jtx = env.jt(jv);
+            BEAST_EXPECT(jtx.stx);
+
+            // Serialize the signed transaction
+            Serializer s;
+            jtx.stx->add(s);
+            SerialIter sit(s.slice());
+            STObject obj(sit, sfTransaction);
+
+            // Replace sender ciphertext with zero-randomness form:
+            // C1 = all zeros (identity element — invalid encoding)
+            // C2 = valid trivial point (simulating mG)
+            Buffer zeroCiphertext(kEcGamalEncryptedTotalLength);
+            std::memset(zeroCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
+            // C2 half: use a valid point so only C1 is the problem
+            auto const& tc = getTrivialCiphertext();
+            std::memcpy(
+                zeroCiphertext.data() + kEcCiphertextComponentLength,
+                tc.data() + kEcCiphertextComponentLength,
+                kEcCiphertextComponentLength);
+            obj.setFieldVL(sfSenderEncryptedAmount, zeroCiphertext);
+
+            // Re-serialize with the original (now-stale) signature
+            Serializer tampered;
+            obj.add(tampered);
+
+            // Signature verification fails because ciphertext fields are
+            // signed — transaction rejected before ZKP verification.
+            auto const jr = env.rpc("submit", strHex(tampered.slice()));
+            BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
+        }
+
+        // -----------------------------------------------------------------
+        // Variant B: Re-signed zero-randomness ciphertext
+        // -----------------------------------------------------------------
+        // Same zero-randomness ciphertext as Variant A (C1 = 0, C2 = mG),
+        // but submitted normally via send() which re-signs the transaction.
+        // Signature verification passes, but preflight's isValidCiphertext
+        // rejects it: the identity element has no valid compressed encoding
+        // on secp256k1, so secp256k1_ec_pubkey_parse fails on C1 = 0.
+        {
+            // Build zero-randomness ciphertext: C1 = all zeros (identity),
+            // C2 = valid trivial point (simulating mG)
+            Buffer zeroCiphertext(kEcGamalEncryptedTotalLength);
+            std::memset(zeroCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
+            auto const& tc = getTrivialCiphertext();
+            std::memcpy(
+                zeroCiphertext.data() + kEcCiphertextComponentLength,
+                tc.data() + kEcCiphertextComponentLength,
+                kEcCiphertextComponentLength);
+
+            mptAlice.send(
+                {.account = bob,
+                 .dest = carol,
+                 .amt = sendAmount,
+                 .senderEncryptedAmt = zeroCiphertext,
+                 .err = temBAD_CIPHERTEXT});
+        }
+
+        // -----------------------------------------------------------------
+        // Variant C: Deterministic ciphertext reuse across transactions
+        // -----------------------------------------------------------------
+        // Construct two transactions using identical deterministic
+        // ciphertexts (same fixed blinding factor).  Even if a valid
+        // proof could be generated for one, it cannot be reused because
+        // the TransactionContextID (which includes account sequence)
+        // differs between transactions.
+        {
+            // First transaction: generate valid proof for sendAmount
+            ConfidentialSendSetup const setup1(mptAlice, bob, carol, alice, sendAmount);
+
+            auto const proof1 = setup1.generateProof(mptAlice, env, bob, carol);
+            if (!BEAST_EXPECT(proof1.has_value()))
+                return;
+
+            // Submit first transaction successfully
+            mptAlice.send(setup1.sendArgs(bob, carol, requireOptionalRef(proof1, "Missing proof")));
+
+            mptAlice.mergeInbox({.account = carol});
+
+            // Second transaction: reuse the same proof from tx1.
+            // The context hash includes the new account sequence, so the
+            // proof generated for the old sequence is invalid.
+            ConfidentialSendSetup const setup2(mptAlice, bob, carol, alice, sendAmount);
+
+            mptAlice.send(setup2.sendArgs(
+                bob, carol, requireOptionalRef(proof1, "Missing proof"), tecBAD_PROOF));
+        }
+    }
+
+    void
+    testSendRerandomizesRecipientInboxAgainstMergeCancellation(FeatureBitset features)
+    {
+        testcase("Send: recipient inbox rerandomization prevents merge cancellation");
+
+        using namespace test::jtx;
+
+        // Derive the deterministic canonical-zero randomness r0 used for
+        // Bob's first spending balance.
+        auto getCanonicalZeroBlindingFactor = [](AccountID const& account, MPTID const& mptID) {
+            Buffer scalar(kEcBlindingFactorLength);
+            std::array hashInput{};
+            std::memcpy(hashInput.data(), "EncZero", 7);
+            std::memcpy(hashInput.data() + 7, account.data(), account.size());
+            std::memcpy(hashInput.data() + 27, mptID.data(), mptID.size());
+
+            for (;;)
+            {
+                unsigned int mdLen = kEcBlindingFactorLength;
+                if (EVP_Digest(
+                        hashInput.data(),
+                        hashInput.size(),
+                        scalar.data(),
+                        &mdLen,
+                        EVP_sha256(),
+                        nullptr) != 1)
+                {
+                    Throw("Failed to derive canonical zero blinding factor");
+                }
+
+                if (secp256k1_ec_seckey_verify(mpt_secp256k1_context(), scalar.data()))
+                    return scalar;
+
+                std::memcpy(hashInput.data(), scalar.data(), scalar.size());
+            }
+        };
+
+        // Pick randomness that would cancel Bob's MergeInbox C1 to infinity
+        // without receiver-side re-randomization.
+        auto negateScalarSum = [](Buffer const& lhs, Buffer const& rhs) {
+            Buffer sum(kEcBlindingFactorLength);
+            Buffer negated(kEcBlindingFactorLength);
+            secp256k1_mpt_scalar_add(sum.data(), lhs.data(), rhs.data());
+            secp256k1_mpt_scalar_negate(negated.data(), sum.data());
+            return negated;
+        };
+
+        // Without an auditor, target Bob's holder inbox. The crafted send
+        // randomness would make MergeInbox hit the point at infinity unless
+        // ConfidentialMPTSend re-randomizes the recipient ciphertext.
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob"), carol("carol");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({.account = bob});
+            mptAlice.authorize({.account = carol});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+            mptAlice.mergeInbox({.account = carol});
+
+            Buffer const convertBlindingFactor = generateBlindingFactor();
+            mptAlice.convert({
+                .account = bob,
+                .amt = 20,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .blindingFactor = convertBlindingFactor,
+            });
+
+            Buffer const canonicalZeroBlindingFactor =
+                getCanonicalZeroBlindingFactor(bob.id(), mptAlice.issuanceID());
+
+            // Holder inbox cancellation happens later in MergeInbox, when
+            // Bob's spending Enc(0; r0) is added to inbox Enc(25; -r0).
+            Buffer const maliciousSendBlindingFactor =
+                negateScalarSum(canonicalZeroBlindingFactor, convertBlindingFactor);
+
+            mptAlice.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 5,
+                .blindingFactor = maliciousSendBlindingFactor,
+            });
+
+            mptAlice.mergeInbox({.account = bob});
+
+            auto const bobSpending =
+                mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
+            BEAST_EXPECT(bobSpending && *bobSpending == 25);
+        }
+
+        // With an auditor, verify the destination auditor balance is also
+        // re-randomized. Auditor balance is updated during send, and this crafted
+        // randomness would otherwise make that homomorphic sum hit infinity without
+        // re-randomization.
+        {
+            Env env{*this, features};
+            Account const alice("alice"), bob("bob"), carol("carol"), auditor("auditor");
+            MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
+
+            mptAlice.create({
+                .ownerCount = 1,
+                .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+            });
+
+            mptAlice.authorize({.account = bob});
+            mptAlice.authorize({.account = carol});
+            mptAlice.pay(alice, bob, 100);
+            mptAlice.pay(alice, carol, 100);
+
+            mptAlice.generateKeyPair(alice);
+            mptAlice.generateKeyPair(bob);
+            mptAlice.generateKeyPair(carol);
+            mptAlice.generateKeyPair(auditor);
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+            });
+
+            mptAlice.convert({
+                .account = carol,
+                .amt = 50,
+                .holderPubKey = mptAlice.getPubKey(carol),
+            });
+            mptAlice.mergeInbox({.account = carol});
+
+            Buffer const convertBlindingFactor = generateBlindingFactor();
+            mptAlice.convert({
+                .account = bob,
+                .amt = 20,
+                .holderPubKey = mptAlice.getPubKey(bob),
+                .blindingFactor = convertBlindingFactor,
+            });
+
+            // This would make the homomorphic sum hit infinity.
+            Buffer const maliciousSendBlindingFactor =
+                negateScalarSum(gMakeZeroBuffer(kEcBlindingFactorLength), convertBlindingFactor);
+
+            mptAlice.send({
+                .account = carol,
+                .dest = bob,
+                .amt = 5,
+                .blindingFactor = maliciousSendBlindingFactor,
+            });
+
+            auto const bobAuditor =
+                mptAlice.getDecryptedBalance(bob, MPTTester::auditorEncryptedBalance);
+            BEAST_EXPECT(bobAuditor && *bobAuditor == 25);
+        }
+    }
+
+    void
+    testWithFeats(FeatureBitset features)
+    {
+        // ConfidentialMPTConvert
+        testConvert(features);
+        testConvertPreflight(features);
+        testConvertInvalidProofContextBinding(features);
+        testConvertPreclaim(features);
+        testConvertWithAuditor(features);
+
+        // ConfidentialMPTMergeInbox
+        testMergeInbox(features);
+        testMergeInboxPreflight(features);
+        testMergeInboxPreclaim(features);
+
+        testSet(features);
+        testSetPreflight(features);
+        testSetPreclaim(features);
+
+        // ConfidentialMPTSend
+        testSend(features);
+        testSendPreflight(features);
+        testSendPreclaim(features);
+        testSendRangeProof(features);
+
+        testSendZeroAmount(features);
+        testSendWithAuditor(features);
+
+        // ConfidentialMPTClawback
+        testClawback(features);
+        testClawbackPreflight(features);
+        testClawbackPreclaim(features);
+        testClawbackProof(features);
+        testClawbackWithAuditor(features);
+        testClawbackInvalidProofContextBinding(features);
+
+        testDelete(features);
+
+        // ConfidentialMPTConvertBack
+        testConvertBack(features);
+        testConvertBackPreflight(features);
+        testConvertBackPreclaim(features);
+        testConvertBackWithAuditor(features);
+        testConvertBackPedersenProof(features);
+        testConvertBackBulletproof(features);
+
+        // Homomorphic operation tests
+        testSendHomomorphicOverflow(features);
+        testConvertBackHomomorphicCiphertextModification(features);
+        testConvertBackHomomorphicUnderflow(features);
+
+        // Invalid curve points
+        testSendInvalidCurvePoints(features);
+        testSendWrongGroupPointInjection(features);
+        testConvertIdentityElementRejection(features);
+        testSendWrongIssuerPublicKey(features);
+
+        // public and private txns
+        testPublicTransfersAfterClearingConfidentialFlag(features);
+
+        // Replay tests
+        testMutatePrivacy(features);
+        testConvertBackInvalidProofContextBinding(features);
+        testConvertBackProofCiphertextBinding(features);
+        testConvertBackProofVersionMismatch(features);
+
+        // Crafted-proof Tests
+        testSendSharedRandomnessViolation(features);
+
+        // Transaction Fee Tests
+        testConfidentialMPTBaseFee(features);
+
+        // TransferFee (transfer rate) Tests
+        testTransferFee(features);
+
+        // Zero knowledge proof tests
+        testSendInvalidProofContextBinding(features);
+        testSendForgedEqualityProof(features);
+        testSendForgedRangeProof(features);
+        testSendNegativeValueMalleability(features);
+        testSendFiatShamirBinding(features);
+        testSendProofComponentReuse(features);
+        testSendSpecialWitnessValues(features);
+        testSendCrossStatementProofSubstitution(features);
+
+        // Ciphertext malleability tests
+        testSendCiphertextMalleability(features);
+        testSendCiphertextNegation(features);
+        testSendCiphertextCombination(features);
+        testSendCiphertextRerandomization(features);
+        testSendZeroRandomnessCiphertext(features);
+        testSendRerandomizesRecipientInboxAgainstMergeCancellation(features);
+    }
+
+public:
+    void
+    run() override
+    {
+        using namespace test::jtx;
+        FeatureBitset const all{testableAmendments()};
+
+        testWithFeats(all);
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(ConfidentialTransfer, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp
index f68f813853..02ee0750d5 100644
--- a/src/test/app/Delegate_test.cpp
+++ b/src/test/app/Delegate_test.cpp
@@ -2747,7 +2747,9 @@ class Delegate_test : public beast::unit_test::Suite
         // DO NOT modify expectedDelegableCount unless all scenarios, including
         // edge cases, have been fully tested and verified.
         // ====================================================================
-        std::size_t const expectedDelegableCount = 51;
+        // Includes the five confidential MPT transaction types, which are
+        // explicitly marked Delegable in transactions.macro.
+        std::size_t const expectedDelegableCount = 56;
 
         BEAST_EXPECTS(
             delegableCount == expectedDelegableCount,
diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp
index 6aa17c7840..9fde52ecb2 100644
--- a/src/test/app/Invariants_test.cpp
+++ b/src/test/app/Invariants_test.cpp
@@ -5176,6 +5176,254 @@ class Invariants_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testConfidentialMPTTransfer()
+    {
+        using namespace test::jtx;
+        testcase << "ValidConfidentialMPToken";
+
+        MPTID mptID;
+
+        // Generate an MPT with privacy, issue 100 tokens to A2.
+        // Perform a confidential conversion to populate encrypted state.
+        auto const precloseConfidential =
+            [&mptID](Account const& a1, Account const& a2, Env& env) -> bool {
+            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+            mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
+            mptID = mpt.issuanceID();
+
+            mpt.authorize({.account = a2});
+            mpt.pay(a1, a2, 100);
+
+            mpt.generateKeyPair(a1);
+            mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
+
+            mpt.generateKeyPair(a2);
+            mpt.convert({
+                .account = a2,
+                .amt = 100,
+                .holderPubKey = mpt.getPubKey(a2),
+            });
+            return true;
+        };
+
+        // badDelete
+        doInvariantCheck(
+            {"MPToken deleted with encrypted fields while COA > 0"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Force an erase of the object while the COA remains 100
+                ac.view().erase(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseConfidential);
+
+        // badConsistency
+        doInvariantCheck(
+            {"MPToken encrypted field existence inconsistency"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Remove one of the required encrypted fields to create a mismatch
+                sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        doInvariantCheck(
+            {"MPToken encrypted field existence inconsistency"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
+                sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
+                sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
+                sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00});
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // requiresPrivacyFlag
+        auto const precloseNoPrivacy = [&mptID](
+                                           Account const& a1, Account const& a2, Env& env) -> bool {
+            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+            // completely omitted the tfMPTCanHoldConfidentialBalance flag here.
+            mpt.create({.flags = tfMPTCanTransfer});
+            mptID = mpt.issuanceID();
+            mpt.authorize({.account = a2});
+            mpt.pay(a1, a2, 100);
+            return true;
+        };
+
+        doInvariantCheck(
+            {"MPToken has encrypted fields but Issuance does not have "
+             "lsfMPTCanHoldConfidentialBalance "
+             "set"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Inject all three encrypted fields consistently (inbox+spending+issuer must be
+                // in sync or badConsistency fires first and masks requiresPrivacyFlag).
+                sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
+                sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0x00});
+                sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseNoPrivacy);
+
+        // badCOA
+        doInvariantCheck(
+            {"Confidential outstanding amount exceeds total outstanding amount"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
+                if (!sleIssuance)
+                    return false;
+                // Total outstanding is natively 100; bloat the COA over 100
+                sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
+                ac.view().update(sleIssuance);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Conservation Violation
+        doInvariantCheck(
+            {"Token conservation violation for MPT"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
+                if (!sleIssuance)
+                    return false;
+
+                sleIssuance->setFieldU64(
+                    sfConfidentialOutstandingAmount,
+                    sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
+                ac.view().update(sleIssuance);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Send/MergeInbox must not change OutstandingAmount (coaDelta == 0)
+        doInvariantCheck(
+            {"Invariant failed: OutstandingAmount changed "
+             "by confidential transaction that should not "
+             "modify it for MPT"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
+                if (!sleIssuance)
+                    return false;
+                sleIssuance->setFieldU64(
+                    sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
+                ac.view().update(sleIssuance);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Send/MergeInbox and zero-COA-delta confidential transactions must not
+        // change public holder MPTAmount.
+        doInvariantCheck(
+            {"Invariant failed: MPTAmount changed by confidential "
+             "transaction that should not modify this field."},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // badVersion
+        doInvariantCheck(
+            {"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
+             "changed"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
+
+                // DO NOT update sfConfidentialBalanceVersion
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Skipping Deleted MPTs (Issuance deleted)
+        auto const precloseOrphan = [&mptID](
+                                        Account const& a1, Account const& a2, Env& env) -> bool {
+            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+            mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
+            mptID = mpt.issuanceID();
+            mpt.authorize({.account = a2});
+
+            // Generate privacy keys and convert 0 amount so Bob has the encrypted fields
+            mpt.generateKeyPair(a1);
+            mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
+            mpt.generateKeyPair(a2);
+            mpt.convert({
+                .account = a2,
+                .amt = 0,
+                .holderPubKey = mpt.getPubKey(a2),
+            });
+
+            // Immediately destroy the issuance. A2's empty, encrypted token object lives on.
+            mpt.destroy();
+            return true;
+        };
+
+        doInvariantCheck(
+            {},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Safely able to erase the deleted token.
+                ac.view().erase(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tesSUCCESS, tesSUCCESS},
+            precloseOrphan);
+    }
+
 public:
     void
     run() override
@@ -5204,6 +5452,7 @@ public:
         testValidPseudoAccounts();
         testValidLoanBroker();
         testVault();
+        testConfidentialMPTTransfer();
         testMPT();
         testInvariantOverwrite(defaultAmendments());
         testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp
index 323184aa36..15e17b4536 100644
--- a/src/test/app/MPToken_test.cpp
+++ b/src/test/app/MPToken_test.cpp
@@ -617,7 +617,8 @@ class MPToken_test : public beast::unit_test::Suite
             // (2)
             mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG});
 
-            if (!features[featureSingleAssetVault] && !features[featureDynamicMPT])
+            if (!features[featureSingleAssetVault] && !features[featureDynamicMPT] &&
+                !features[featureConfidentialTransfer])
             {
                 // test invalid flags - nothing is being changed
                 mptAlice.set({.account = alice, .flags = 0x00000000, .err = tecNO_PERMISSION});
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index 6fefcfc404..dd28c7ec6e 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -39,6 +39,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -5902,6 +5903,46 @@ class Vault_test : public beast::unit_test::Suite
         runTest(amendments);
     }
 
+    void
+    testRemoveEmptyHoldingConfidentialBalances()
+    {
+        testcase("removeEmptyHolding keeps MPToken with confidential balances");
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+
+        Account const issuer{"issuer"};
+        Account const holder{"holder"};
+        MPTTester mpt{env, issuer, {.holders = {holder}}};
+        mpt.create({.authorize = MPTCreate::allHolders});
+
+        auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id());
+        auto const encryptedBalanceFields = {
+            &sfConfidentialBalanceInbox,
+            &sfConfidentialBalanceSpending,
+            &sfIssuerEncryptedBalance,
+            &sfAuditorEncryptedBalance};
+
+        env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
+            for (auto const field : encryptedBalanceFields)
+            {
+                Sandbox sb(&view, TapNone);
+                auto const token = sb.peek(tokenKeylet);
+                if (!BEAST_EXPECT(token))
+                    return false;
+
+                token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
+                sb.update(token);
+
+                BEAST_EXPECT(
+                    removeEmptyHolding(sb, holder.id(), MPTIssue(mpt.issuanceID()), j) ==
+                    tecHAS_OBLIGATIONS);
+                BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr);
+            }
+            return true;
+        });
+    }
+
     // -----------------------------------------------------------------------
     // Helpers and tests: sole-shareholder / stuck-depositor (XLS-0065 +
     // fixCleanup3_2_0). The vault-level withdraw behavior is tested here;
@@ -8073,6 +8114,7 @@ public:
         testAssetsMaximum();
         testBug6LimitBypassWithShares();
         testRemoveEmptyHoldingLockedAmount();
+        testRemoveEmptyHoldingConfidentialBalances();
 
         testWithdrawSoleShareholderFixedAssetExit(all_ - fixCleanup3_2_0);
         testWithdrawSoleShareholderFixedAssetExit(all_);
diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h
new file mode 100644
index 0000000000..b758683da6
--- /dev/null
+++ b/src/test/jtx/ConfidentialTransfer.h
@@ -0,0 +1,496 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class ConfidentialTransferTestBase : public beast::unit_test::Suite
+{
+protected:
+    template 
+    static T
+    requireOptional(std::optional value, char const* message)
+    {
+        if (!value)
+            Throw(message);
+        return std::move(*value);
+    }
+
+    template 
+    static T const&
+    requireOptionalRef(std::optional const& value, char const* message)
+    {
+        if (!value)
+            Throw(message);
+        return *value;
+    }
+
+    // Offset where the bulletproof begins in a send proof blob.
+    // Proof layout: [compact_sigma | bulletproof]
+    static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength;
+
+    // Generate a forged aggregated bulletproof (double bulletproof) for
+    // the given values and blinding factors. Used to test that splicing
+    // a bulletproof claiming a different remaining balance is rejected.
+    // secp256k1 convention: returns 1 on success, 0 on failure.
+    static Buffer
+    getForgedBulletproof(
+        std::array const& values,
+        std::array const& blindingFactors,
+        uint256 const& contextHash)
+    {
+        auto* const ctx = mpt_secp256k1_context();
+
+        secp256k1_pubkey h;
+        secp256k1_mpt_get_h_generator(ctx, &h);
+
+        Buffer proof(kEcDoubleBulletproofLength);
+        size_t proofLen = kEcDoubleBulletproofLength;
+
+        unsigned char blindings[64];
+        std::memcpy(blindings, blindingFactors[0].data(), 32);
+        std::memcpy(blindings + 32, blindingFactors[1].data(), 32);
+
+        if (secp256k1_bulletproof_prove_agg(
+                ctx,
+                proof.data(),
+                &proofLen,
+                values.data(),
+                blindings,
+                2,
+                &h,
+                contextHash.data()) == 0)
+            Throw("Failed to generate forged bulletproof");
+
+        return proof;
+    }
+
+    // Get a bad ciphertext with valid structure but cryptographic invalid for
+    // testing purposes. For preflight test purposes.
+    static Buffer const&
+    getBadCiphertext()
+    {
+        static Buffer const kBadCiphertext = []() {
+            Buffer buf(kEcGamalEncryptedTotalLength);
+            std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength);
+
+            buf.data()[0] = kEcCompressedPrefixEvenY;
+            buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
+            return buf;
+        }();
+
+        return kBadCiphertext;
+    }
+
+    // Get a trivial buffer that is structurally and mathematically valid, but
+    // contains invalid data that does not match the ledger state. For preclaim
+    // test purposes.
+    static Buffer const&
+    getTrivialCiphertext()
+    {
+        static Buffer const kTrivialCiphertext = []() {
+            Buffer buf(kEcGamalEncryptedTotalLength);
+            std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength);
+
+            buf.data()[0] = kEcCompressedPrefixEvenY;
+            buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
+
+            buf.data()[kEcCiphertextComponentLength - 1] = 0x01;
+            buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
+
+            return buf;
+        }();
+
+        return kTrivialCiphertext;
+    }
+
+    // Returns a valid compressed EC point (33 bytes) that can pass preflight
+    // validation but contains invalid data for preclaim test purposes.
+    static Buffer const&
+    getTrivialCommitment()
+    {
+        static Buffer const kTrivialCommitment = []() {
+            Buffer buf(kEcPedersenCommitmentLength);
+            std::memset(buf.data(), 0, kEcPedersenCommitmentLength);
+
+            buf.data()[0] = kEcCompressedPrefixEvenY;
+            // Set last byte to make it a valid x-coordinate on the curve
+            buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
+
+            return buf;
+        }();
+
+        return kTrivialCommitment;
+    }
+
+    static std::string
+    getTrivialSendProofHex()
+    {
+        Buffer buf(kEcSendProofLength);
+        std::memset(buf.data(), 0, kEcSendProofLength);
+
+        for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength)
+        {
+            buf.data()[i] = kEcCompressedPrefixEvenY;
+            if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength)
+                buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01;
+        }
+
+        return strHex(buf);
+    }
+
+    // Helper struct to encapsulate common setup for integration tests.
+    struct ConfidentialSendSetup
+    {
+        // Constants
+        uint64_t sendAmount;
+        size_t nRecipients;
+        uint32_t version;
+
+        // Blinding factors
+        Buffer blindingFactor;
+        Buffer amountBlindingFactor;
+        Buffer balanceBlindingFactor;
+
+        // Encrypted amounts
+        Buffer senderAmt;
+        Buffer destAmt;
+        Buffer issuerAmt;
+        std::optional auditorAmt;
+
+        // Commitments
+        Buffer amountCommitment;
+
+        // Long-lived pub key buffers (to avoid dangling Slice)
+        Buffer senderPubKey;
+        Buffer destPubKey;
+        Buffer issuerPubKey;
+        std::optional auditorPubKey;
+
+        // Balance data
+        uint64_t prevSpending;
+        Buffer prevEncryptedSpending;
+
+        // Balance commitment (declared after prevSpending for init order)
+        Buffer balanceCommitment;
+
+        // Recipients vector
+        std::vector recipients;
+
+        // Constructor that performs all common setup
+        ConfidentialSendSetup(
+            test::jtx::MPTTester& mpt,
+            test::jtx::Account const& sender,
+            test::jtx::Account const& dest,
+            test::jtx::Account const& issuer,
+            uint64_t amount,
+            std::optional> auditor = std::nullopt)
+            : sendAmount(amount)
+            , nRecipients(auditor ? 4 : 3)
+            , version(mpt.getMPTokenVersion(sender))
+            , blindingFactor(generateBlindingFactor())
+            , amountBlindingFactor(blindingFactor)
+            , balanceBlindingFactor(generateBlindingFactor())
+            , senderAmt(mpt.encryptAmount(sender, amount, blindingFactor))
+            , destAmt(mpt.encryptAmount(dest, amount, blindingFactor))
+            , issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor))
+            , auditorAmt(
+                  auditor ? std::optional(
+                                mpt.encryptAmount(auditor->get(), amount, blindingFactor))
+                          : std::nullopt)
+            , amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor))
+            , senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key"))
+            , destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key"))
+            , issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key"))
+            , auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt)
+            , prevSpending(requireOptional(
+                  mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
+                  "Missing sender spending balance"))
+            , prevEncryptedSpending(requireOptional(
+                  mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
+                  "Missing sender encrypted spending balance"))
+            , balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor))
+        {
+            recipients.push_back({
+                .publicKey = Slice(senderPubKey),
+                .encryptedAmount = senderAmt,
+            });
+            recipients.push_back({
+                .publicKey = Slice(destPubKey),
+                .encryptedAmount = destAmt,
+            });
+            recipients.push_back({
+                .publicKey = Slice(issuerPubKey),
+                .encryptedAmount = issuerAmt,
+            });
+            if (auditor)
+            {
+                recipients.push_back({
+                    .publicKey =
+                        Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")),
+                    .encryptedAmount =
+                        requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"),
+                });
+            }
+        }
+
+        // Generate proof with current account sequence
+        std::optional
+        generateProof(
+            test::jtx::MPTTester& mpt,
+            test::jtx::Env& env,
+            test::jtx::Account const& sender,
+            test::jtx::Account const& dest) const
+        {
+            auto const ctxHash = getSendContextHash(
+                sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version);
+
+            return mpt.getConfidentialSendProof(
+                sender,
+                sendAmount,
+                recipients,
+                blindingFactor,
+                ctxHash,
+                {
+                    .pedersenCommitment = amountCommitment,
+                    .amt = sendAmount,
+                    .encryptedAmt = senderAmt,
+                    .blindingFactor = amountBlindingFactor,
+                },
+                {
+                    .pedersenCommitment = balanceCommitment,
+                    .amt = prevSpending,
+                    .encryptedAmt = prevEncryptedSpending,
+                    .blindingFactor = balanceBlindingFactor,
+                });
+        }
+
+        [[nodiscard]] test::jtx::MPTConfidentialSend
+        sendArgs(
+            test::jtx::Account const& sender,
+            test::jtx::Account const& dest,
+            Buffer const& proof,
+            std::optional err = std::nullopt) const
+        {
+            return {
+                .account = sender,
+                .dest = dest,
+                .amt = sendAmount,
+                .proof = strHex(proof),
+                .senderEncryptedAmt = senderAmt,
+                .destEncryptedAmt = destAmt,
+                .issuerEncryptedAmt = issuerAmt,
+                .auditorEncryptedAmt = auditorAmt,
+                .amountCommitment = amountCommitment,
+                .balanceCommitment = balanceCommitment,
+                .err = err,
+            };
+        }
+    };
+
+    // Helper that wraps the boilerplate setup: Env + MPT creation, funding, key
+    // generation, and seeding each holder with a confidential balance.
+    // The caller supplies the issuer and any number of holders.
+    struct ConfidentialEnv
+    {
+        // Per-holder configuration: the account, how much MPT to fund it
+        // with, and how much of that to convert to a confidential balance.
+        struct HolderInit
+        {
+            test::jtx::Account account;
+            std::uint64_t payAmount = 1000;
+            std::uint64_t convertAmount = 100;
+        };
+
+        test::jtx::MPTTester mpt;
+
+        ConfidentialEnv(
+            test::jtx::Env& env,
+            test::jtx::Account const& issuer,
+            std::vector const& holders,
+            std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
+            std::optional auditor = std::nullopt)
+            : mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}}
+        {
+            mpt.create({.ownerCount = 1, .flags = flags});
+
+            for (auto const& h : holders)
+            {
+                mpt.authorize({.account = h.account});
+                if ((flags & tfMPTRequireAuth) != 0)
+                    mpt.authorize({.account = issuer, .holder = h.account});
+                mpt.pay(issuer, h.account, h.payAmount);
+            }
+
+            mpt.generateKeyPair(issuer);
+            for (auto const& h : holders)
+                mpt.generateKeyPair(h.account);
+            if (auditor)
+                mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor"));
+
+            mpt.set({
+                .account = issuer,
+                .issuerPubKey = mpt.getPubKey(issuer),
+                .auditorPubKey = auditor
+                    ? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor"))
+                    : std::optional{},
+            });
+
+            for (auto const& h : holders)
+            {
+                mpt.convert({
+                    .account = h.account,
+                    .amt = h.convertAmount,
+                    .holderPubKey = mpt.getPubKey(h.account),
+                });
+                mpt.mergeInbox({.account = h.account});
+            }
+        }
+
+    private:
+        static std::vector
+        extractAccounts(std::vector const& holders)
+        {
+            std::vector accounts;
+            accounts.reserve(holders.size());
+            for (auto const& h : holders)
+                accounts.push_back(h.account);
+            return accounts;
+        }
+    };
+
+    // Set up an MPT environment suitable for batch testing.
+    // alice is issuer; bob has 'bobAmt' in confidential spending; carol has
+    // 'carolAmt' in confidential spending; dave is initialised with pubkey but
+    // zero spending/inbox.
+    static void
+    setupBatchEnv(
+        test::jtx::MPTTester& mpt,
+        test::jtx::Account const& alice,
+        test::jtx::Account const& bob,
+        test::jtx::Account const& carol,
+        test::jtx::Account const& dave,
+        std::uint64_t bobAmt,
+        std::uint64_t carolAmt)
+    {
+        using namespace test::jtx;
+        mpt.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
+        });
+        mpt.authorize({.account = bob});
+        mpt.authorize({.account = carol});
+        mpt.authorize({.account = dave});
+
+        if (bobAmt > 0)
+            mpt.pay(alice, bob, bobAmt);
+        if (carolAmt > 0)
+            mpt.pay(alice, carol, carolAmt);
+
+        mpt.generateKeyPair(alice);
+        mpt.generateKeyPair(bob);
+        mpt.generateKeyPair(carol);
+        mpt.generateKeyPair(dave);
+
+        mpt.set({
+            .account = alice,
+            .issuerPubKey = mpt.getPubKey(alice),
+        });
+
+        if (bobAmt > 0)
+        {
+            mpt.convert({
+                .account = bob,
+                .amt = bobAmt,
+                .holderPubKey = mpt.getPubKey(bob),
+            });
+            mpt.mergeInbox({.account = bob});
+        }
+        else
+        {
+            mpt.convert({
+                .account = bob,
+                .amt = 0,
+                .holderPubKey = mpt.getPubKey(bob),
+            });
+        }
+
+        if (carolAmt > 0)
+        {
+            mpt.convert({
+                .account = carol,
+                .amt = carolAmt,
+                .holderPubKey = mpt.getPubKey(carol),
+            });
+            mpt.mergeInbox({.account = carol});
+        }
+        else
+        {
+            mpt.convert({
+                .account = carol,
+                .amt = 0,
+                .holderPubKey = mpt.getPubKey(carol),
+            });
+        }
+
+        // dave: register pubkey only (0 spending/inbox)
+        mpt.convert({
+            .account = dave,
+            .amt = 0,
+            .holderPubKey = mpt.getPubKey(dave),
+        });
+    }
+};
+
+}  // namespace xrpl
diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h
index a80ce46b9c..0e78d409fa 100644
--- a/src/test/jtx/batch.h
+++ b/src/test/jtx/batch.h
@@ -14,18 +14,46 @@
 #include 
 #include 
 
-/** Batch operations */
+/** @brief Helpers for constructing Batch test transactions. */
 namespace xrpl::test::jtx::batch {
 
-/** Calculate Batch Fee. */
+/**
+ * @brief Calculate the expected outer Batch transaction fee.
+ *
+ * @param env The test environment providing ledger fee settings.
+ * @param numSigners Number of outer transaction signers.
+ * @param txns Number of inner transactions in the batch.
+ * @return The expected Batch fee.
+ */
 XRPAmount
 calcBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32_t const& txns = 0);
 
-/** Batch. */
+/**
+ * @brief Calculate the expected Batch fee when inner transactions are
+ * confidential MPT transactions.
+ *
+ * @param env The test environment providing ledger fee settings.
+ * @param numSigners Number of outer transaction signers.
+ * @param txns Number of confidential MPT inner transactions in the batch.
+ * @return The expected Batch fee including confidential transaction fee
+ *         multipliers.
+ */
+XRPAmount
+calcConfidentialBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32_t const& txns = 0);
+
+/**
+ * @brief Build an outer Batch transaction JSON object.
+ *
+ * @param account The account submitting the outer Batch transaction.
+ * @param seq The sequence number for the outer Batch transaction.
+ * @param fee The fee to set on the outer Batch transaction.
+ * @param flags The transaction flags to set.
+ * @return The outer Batch transaction JSON object.
+ */
 json::Value
 outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags);
 
-/** Adds a new Batch Txn on a JTx and autofills. */
+/** @brief Adds an inner Batch transaction to a JTx and autofills it. */
 class Inner
 {
 private:
@@ -75,7 +103,7 @@ public:
     }
 };
 
-/** Set a batch signature on a JTx. */
+/** @brief Sets the Batch transaction signers on a JTx. */
 class Sig
 {
 public:
@@ -98,7 +126,7 @@ public:
     operator()(Env&, JTx& jt) const;
 };
 
-/** Set a batch nested multi-signature on a JTx. */
+/** @brief Sets a nested multi-signature for a Batch transaction on a JTx. */
 class Msig
 {
 public:
diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp
index 2f7a67b45a..66ca0c7d54 100644
--- a/src/test/jtx/impl/batch.cpp
+++ b/src/test/jtx/impl/batch.cpp
@@ -11,6 +11,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -37,6 +38,16 @@ calcBatchFee(test::jtx::Env const& env, uint32_t const& numSigners, uint32_t con
     return ((numSigners + 2) * feeDrops) + feeDrops * txns;
 }
 
+XRPAmount
+calcConfidentialBatchFee(
+    test::jtx::Env const& env,
+    uint32_t const& numSigners,
+    uint32_t const& txns)
+{
+    XRPAmount const feeDrops = env.current()->fees().base;
+    return ((numSigners + 2) * feeDrops) + feeDrops * (kConfidentialFeeMultiplier + 1) * txns;
+}
+
 // Batch.
 json::Value
 outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags)
diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp
index 84c5b5fbec..dddfc88c7f 100644
--- a/src/test/jtx/impl/mpt.cpp
+++ b/src/test/jtx/impl/mpt.cpp
@@ -10,6 +10,8 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -18,8 +20,10 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -27,9 +31,17 @@
 #include 
 #include 
 
+#include 
+
+#include 
+#include 
+#include 
+
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -40,6 +52,47 @@
 #include 
 
 namespace xrpl::test::jtx {
+namespace {
+
+constexpr std::uint64_t kElGamalDecryptRangeLow = 0;
+constexpr std::uint64_t kElGamalDecryptRangeHigh = 3000;
+
+/**
+ * @brief Returns a reference to the value held by an optional, throwing if it
+ *        is not an optional.
+ *
+ * @param opt The optional to unwrap.
+ * @param what Description used in the thrown exception if opt is empty.
+ * @return A const reference to the contained value.
+ */
+template 
+[[nodiscard]] T const&
+requireValue(std::optional const& opt, char const* what)
+{
+    if (!opt)
+        Throw(what);
+    return *opt;
+}
+
+/**
+ * @brief Helper function to convert a PedersenProofParams into the C library struct.
+ *
+ * @param params The Pedersen commitment proof parameters.
+ * @return The equivalent mpt_pedersen_proof_params for use with the C library.
+ */
+mpt_pedersen_proof_params
+makePedersenParams(PedersenProofParams const& params)
+{
+    mpt_pedersen_proof_params res{};
+    std::memcpy(
+        res.pedersen_commitment, params.pedersenCommitment.data(), kMPT_PEDERSEN_COMMIT_SIZE);
+    res.amount = params.amt;
+    std::memcpy(res.ciphertext, params.encryptedAmt.data(), kMPT_ELGAMAL_TOTAL_SIZE);
+    std::memcpy(res.blinding_factor, params.blindingFactor.data(), kMPT_BLINDING_FACTOR_SIZE);
+    return res;
+}
+
+}  // namespace
 
 struct MPTSetFlagMapping
 {
@@ -88,13 +141,20 @@ MPTTester::makeHolders(std::vector const& holders)
 }
 
 MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg)
-    : env_(env), issuer_(std::move(issuer)), holders_(makeHolders(arg.holders)), close_(arg.close)
+    : env_(env)
+    , issuer_(std::move(issuer))
+    , holders_(makeHolders(arg.holders))
+    , auditor_(arg.auditor)
+    , close_(arg.close)
 {
     if (arg.fund)
     {
         env_.fund(arg.xrp, issuer_);
         for (auto const& it : holders_)
             env_.fund(arg.xrpHolders, it.second);
+
+        if (arg.auditor)
+            env_.fund(arg.xrp, *arg.auditor);
     }
     if (close_)
         env.close();
@@ -107,6 +167,9 @@ MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg)
                 Throw("Issuer can't be holder");
             env_.require(Owners(it.second, 0));
         }
+
+        if (arg.auditor)
+            env_.require(Owners(*arg.auditor, 0));
     }
     if (arg.create)
         create(*arg.create);
@@ -148,7 +211,12 @@ MPTTester::MPTTester(MPTInitDef const& arg)
     : MPTTester{
           arg.env,
           arg.issuer,
-          MPTInit{.fund = arg.fund, .close = arg.close, .create = makeMPTCreate(arg)}}
+          MPTInit{
+              .auditor = arg.auditor,
+              .fund = arg.fund,
+              .close = arg.close,
+              .create = makeMPTCreate(arg),
+          }}
 {
 }
 
@@ -401,6 +469,10 @@ MPTTester::setJV(MPTSet const& arg)
         jv[sfTransferFee] = *arg.transferFee;
     if (arg.metadata)
         jv[sfMPTokenMetadata] = strHex(*arg.metadata);
+    if (arg.issuerPubKey)
+        jv[sfIssuerEncryptionKey] = strHex(*arg.issuerPubKey);
+    if (arg.auditorPubKey)
+        jv[sfAuditorEncryptionKey] = strHex(*arg.auditorPubKey);
     jv[sfTransactionType] = jss::MPTokenIssuanceSet;
 
     return jv;
@@ -419,42 +491,92 @@ MPTTester::set(MPTSet const& arg)
          .transferFee = arg.transferFee,
          .metadata = arg.metadata,
          .delegate = arg.delegate,
-         .domainID = arg.domainID});
+         .domainID = arg.domainID,
+         .issuerPubKey = arg.issuerPubKey,
+         .auditorPubKey = arg.auditorPubKey});
     if (submit(arg, jv) == tesSUCCESS && ((arg.flags.value_or(0) != 0u) || arg.mutableFlags))
     {
-        auto require = [&](std::optional const& holder, bool unchanged) {
-            auto flags = getFlags(holder);
-            if (!unchanged)
-            {
-                if (arg.flags)
+        if (((arg.flags.value_or(0) != 0u) || arg.mutableFlags))
+        {
+            auto require = [&](std::optional const& holder, bool unchanged) {
+                auto flags = getFlags(holder);
+                if (!unchanged)
                 {
-                    if (*arg.flags & tfMPTLock)
+                    if (arg.flags)
                     {
-                        flags |= lsfMPTLocked;
-                    }
-                    else if (*arg.flags & tfMPTUnlock)
-                    {
-                        flags &= ~lsfMPTLocked;
-                    }
-                }
-
-                if (arg.mutableFlags)
-                {
-                    for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings)
-                    {
-                        if ((*arg.mutableFlags & setFlag) != 0u)
+                        if (*arg.flags & tfMPTLock)
                         {
-                            flags |= ledgerFlag;
+                            flags |= lsfMPTLocked;
+                        }
+                        else if (*arg.flags & tfMPTUnlock)
+                        {
+                            flags &= ~lsfMPTLocked;
                         }
                     }
+
+                    if (arg.mutableFlags)
+                    {
+                        for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings)
+                        {
+                            if ((*arg.mutableFlags & setFlag) != 0u)
+                            {
+                                flags |= ledgerFlag;
+                            }
+                        }
+
+                        if (*arg.mutableFlags & tmfMPTSetCanHoldConfidentialBalance)
+                            flags |= tfMPTCanHoldConfidentialBalance;
+                    }
                 }
+                env_.require(MptFlags(*this, flags, holder));
+            };
+            if (arg.account)
+                require(std::nullopt, arg.holder.has_value());
+            if (auto const account = (arg.holder ? std::get_if(&(*arg.holder)) : nullptr))
+                require(*account, false);
+
+            if (arg.issuerPubKey)
+            {
+                env_.require(RequireAny([&]() -> bool {
+                    return forObject([&](SLEP const& sle) -> bool {
+                        if (sle)
+                        {
+                            auto const issuerPubKey = getPubKey(issuer_);
+                            if (!issuerPubKey)
+                            {
+                                Throw(
+                                    "MPTTester::set: issuer's pubkey is not set");
+                            }
+
+                            return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey);
+                        }
+                        return false;
+                    });
+                }));
             }
-            env_.require(MptFlags(*this, flags, holder));
-        };
-        if (arg.account)
-            require(std::nullopt, arg.holder.has_value());
-        if (auto const account = (arg.holder ? std::get_if(&(*arg.holder)) : nullptr))
-            require(*account, false);
+            if (arg.auditorPubKey)
+            {
+                env_.require(RequireAny([&]() -> bool {
+                    return forObject([&](SLEP const& sle) -> bool {
+                        if (sle)
+                        {
+                            if (!auditor_.has_value())
+                                Throw("MPTTester::set: auditor is not set");
+
+                            auto const auditorPubKey = getPubKey(*auditor_);
+                            if (!auditorPubKey)
+                            {
+                                Throw(
+                                    "MPTTester::set: auditor's pubkey is not set");
+                            }
+
+                            return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey);
+                        }
+                        return false;
+                    });
+                }));
+            }
+        }
     }
 }
 
@@ -495,6 +617,14 @@ MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const
         [&](SLEP const& sle) { return expectedAmount == (*sle)[sfOutstandingAmount]; });
 }
 
+[[nodiscard]] bool
+MPTTester::checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const
+{
+    return forObject([&](SLEP const& sle) {
+        return expectedAmount == (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
+    });
+}
+
 [[nodiscard]] bool
 MPTTester::checkFlags(uint32_t const expectedFlags, std::optional const& holder) const
 {
@@ -641,6 +771,238 @@ MPTTester::getBalance(Account const& account) const
     return 0;
 }
 
+std::int64_t
+MPTTester::getIssuanceConfidentialBalance() const
+{
+    if (!id_)
+        Throw("MPT has not been created");
+
+    if (auto const sle = env_.le(keylet::mptokenIssuance(*id_)))
+        return (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
+
+    return 0;
+}
+
+std::optional
+MPTTester::getClawbackProof(
+    Account const& holder,
+    std::uint64_t amount,
+    Buffer const& privateKey,
+    uint256 const& contextHash) const
+{
+    if (!id_)
+        Throw("MPT has not been created");
+
+    auto const sleHolder = env_.le(keylet::mptoken(*id_, holder.id()));
+    auto const sleIssuance = env_.le(keylet::mptokenIssuance(*id_));
+
+    if (!sleHolder || !sleIssuance)
+        return std::nullopt;
+
+    auto const ciphertextBlob = sleHolder->getFieldVL(sfIssuerEncryptedBalance);
+    if (ciphertextBlob.size() != kEcGamalEncryptedTotalLength)
+        return std::nullopt;
+
+    auto const pubKeyBlob = sleIssuance->getFieldVL(sfIssuerEncryptionKey);
+    if (pubKeyBlob.size() != kEcPubKeyLength)
+        return std::nullopt;
+
+    Buffer proof(kEcClawbackProofLength);
+
+    if (mpt_get_clawback_proof(
+            privateKey.data(),
+            pubKeyBlob.data(),
+            contextHash.data(),
+            amount,
+            ciphertextBlob.data(),
+            proof.data()) != 0)
+    {
+        return std::nullopt;
+    }
+
+    return proof;
+}
+
+std::optional
+MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const
+{
+    auto const pubKey = getPubKey(account);
+    if (!pubKey || pubKey->size() != kEcPubKeyLength)
+        return std::nullopt;
+
+    auto const privKey = getPrivKey(account);
+    if (requireValue(privKey, "privKey").size() != kEcPrivKeyLength)
+        return std::nullopt;
+
+    Buffer proof(kEcSchnorrProofLength);
+
+    if (mpt_get_convert_proof(
+            requireValue(pubKey, "pubKey").data(),
+            requireValue(privKey, "privKey").data(),
+            ctxHash.data(),
+            proof.data()) != 0)
+        return std::nullopt;
+
+    return proof;
+}
+
+std::optional
+MPTTester::getConfidentialSendProof(
+    Account const& sender,
+    std::uint64_t const amount,
+    std::vector const& recipients,
+    Slice const& blindingFactor,
+    uint256 const& contextHash,
+    PedersenProofParams const& amountParams,
+    PedersenProofParams const& balanceParams) const
+{
+    auto const pedersenBalanceParams = makePedersenParams(balanceParams);
+
+    if (blindingFactor.size() != kEcBlindingFactorLength)
+        return std::nullopt;
+
+    auto const senderPrivKey = getPrivKey(sender);
+    if (!senderPrivKey)
+        return std::nullopt;
+
+    auto const senderPubKey = getPubKey(sender);
+    if (!senderPubKey || senderPubKey->size() != kEcPubKeyLength)
+        return std::nullopt;
+
+    if (amountParams.pedersenCommitment.size() != kEcPedersenCommitmentLength)
+        return std::nullopt;
+
+    // Build mpt_confidential_participant array
+    std::vector participants(recipients.size());
+    for (size_t i = 0; i < recipients.size(); ++i)
+    {
+        auto const& r = recipients[i];
+        if (r.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
+            r.publicKey.size() != kEcPubKeyLength)
+        {
+            return std::nullopt;
+        }
+        std::memcpy(participants[i].pubkey, r.publicKey.data(), kEcPubKeyLength);
+        std::memcpy(
+            participants[i].ciphertext, r.encryptedAmount.data(), kEcGamalEncryptedTotalLength);
+    }
+
+    size_t proofLen = kEcSendProofLength;
+    Buffer proof(proofLen);
+
+    if (mpt_get_confidential_send_proof(
+            senderPrivKey->data(),
+            senderPubKey->data(),
+            amount,
+            participants.data(),
+            recipients.size(),
+            blindingFactor.data(),
+            contextHash.data(),
+            amountParams.pedersenCommitment.data(),
+            &pedersenBalanceParams,
+            proof.data(),
+            &proofLen) != 0)
+        return std::nullopt;
+
+    return proof;
+}
+
+Buffer
+MPTTester::getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor)
+{
+    // Blinding factor (rho) must be a 32-byte scalar
+    if (pedersenBlindingFactor.size() != kEcBlindingFactorLength)
+        Throw("Invalid blinding factor size");
+
+    // secp256k1_mpt_pedersen_commit doesn't handle amount 0, return a trivial
+    // valid commitment for test purposes
+    if (amount == 0)
+    {
+        Buffer buf(kEcPedersenCommitmentLength);
+        std::memset(buf.data(), 0, kEcPedersenCommitmentLength);
+        buf.data()[0] = kEcCompressedPrefixEvenY;
+        buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
+        return buf;
+    }
+
+    Buffer buf(kEcPedersenCommitmentLength);
+
+    if (mpt_get_pedersen_commitment(amount, pedersenBlindingFactor.data(), buf.data()) != 0)
+        Throw("Pedersen commitment generation failed");
+
+    return buf;
+}
+
+Buffer
+MPTTester::getConvertBackProof(
+    Account const& holder,
+    std::uint64_t const amount,
+    uint256 const& contextHash,
+    PedersenProofParams const& pcParams) const
+{
+    // Expected total proof length: compact sigma proof (128 bytes) + single bulletproof (688 bytes)
+    std::size_t constexpr kExpectedProofLength = kEcConvertBackProofLength;
+
+    auto const sleMptoken = env_.le(keylet::mptoken(issuanceID(), holder.id()));
+    if (!sleMptoken || !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
+        return gMakeZeroBuffer(kExpectedProofLength);
+
+    auto const holderPubKey = getPubKey(holder);
+    auto const holderPrivKey = getPrivKey(holder);
+
+    if (!holderPubKey || !holderPrivKey)
+        return gMakeZeroBuffer(kExpectedProofLength);
+
+    auto const pedersenParams = makePedersenParams(pcParams);
+    Buffer proof(kExpectedProofLength);
+
+    if (mpt_get_convert_back_proof(
+            holderPrivKey->data(),
+            holderPubKey->data(),
+            contextHash.data(),
+            amount,
+            &pedersenParams,
+            proof.data()) != 0)
+        return gMakeZeroBuffer(kExpectedProofLength);
+
+    return proof;
+}
+
+std::optional
+MPTTester::getEncryptedBalance(Account const& account, EncryptedBalanceType option) const
+{
+    if (!id_)
+        Throw("MPT has not been created");
+
+    if (auto const sle = env_.le(keylet::mptoken(*id_, account.id())))
+    {
+        if (option == holderEncryptedInbox && sle->isFieldPresent(sfConfidentialBalanceInbox))
+        {
+            return Buffer(
+                (*sle)[sfConfidentialBalanceInbox].data(),
+                (*sle)[sfConfidentialBalanceInbox].size());
+        }
+        if (option == holderEncryptedSpending && sle->isFieldPresent(sfConfidentialBalanceSpending))
+        {
+            return Buffer(
+                (*sle)[sfConfidentialBalanceSpending].data(),
+                (*sle)[sfConfidentialBalanceSpending].size());
+        }
+        if (option == issuerEncryptedBalance && sle->isFieldPresent(sfIssuerEncryptedBalance))
+        {
+            return Buffer(
+                (*sle)[sfIssuerEncryptedBalance].data(), (*sle)[sfIssuerEncryptedBalance].size());
+        }
+        if (option == auditorEncryptedBalance && sle->isFieldPresent(sfAuditorEncryptedBalance))
+        {
+            return Buffer(
+                (*sle)[sfAuditorEncryptedBalance].data(), (*sle)[sfAuditorEncryptedBalance].size());
+        }
+    }
+
+    return {};
+}
+
 std::uint32_t
 MPTTester::getFlags(std::optional const& holder) const
 {
@@ -667,4 +1029,1515 @@ MPTTester::operator()(std::int64_t amount) const
     return MPT("", issuanceID())(amount);
 }
 
+template 
+void
+MPTTester::fillConversionCiphertexts(
+    T const& arg,
+    json::Value& jv,
+    Buffer& holderCiphertext,
+    Buffer& issuerCiphertext,
+    std::optional& auditorCiphertext,
+    Buffer& blindingFactor) const
+{
+    blindingFactor = arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor();
+
+    // Handle Holder
+    if (arg.holderEncryptedAmt)
+    {
+        holderCiphertext = *arg.holderEncryptedAmt;
+    }
+    else
+    {
+        holderCiphertext = encryptAmount(
+            requireValue(arg.account, "account"), requireValue(arg.amt, "amt"), blindingFactor);
+    }
+
+    jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext);
+
+    // Handle Issuer
+    if (arg.issuerEncryptedAmt)
+    {
+        issuerCiphertext = *arg.issuerEncryptedAmt;
+    }
+    else
+    {
+        issuerCiphertext = encryptAmount(issuer_, requireValue(arg.amt, "amt"), blindingFactor);
+    }
+
+    jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext);
+
+    // Handle Auditor
+    if (arg.auditorEncryptedAmt)
+    {
+        auditorCiphertext = *arg.auditorEncryptedAmt;
+    }
+    else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false))
+    {
+        auditorCiphertext = encryptAmount(
+            requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor);
+    }
+
+    // Update auditor JSON only if ciphertext exists
+    if (auditorCiphertext)
+        jv[sfAuditorEncryptedAmount.jsonName] = strHex(*auditorCiphertext);
+}
+
+void
+MPTTester::convert(MPTConvert const& arg)
+{
+    json::Value jv;
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+
+    jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    if (arg.amt)
+        jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
+    if (arg.holderPubKey)
+        jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey);
+
+    Buffer holderCiphertext;
+    Buffer issuerCiphertext;
+    std::optional auditorCiphertext;
+    Buffer blindingFactor;
+
+    fillConversionCiphertexts(
+        arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
+
+    jv[sfBlindingFactor.jsonName] = strHex(blindingFactor);
+    if (arg.proof)
+    {
+        jv[sfZKProof.jsonName] = *arg.proof;
+    }
+    else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value()))
+    {
+        // whether to automatically generate and attach a Schnorr proof:
+        // if fillSchnorrProof is explicitly set, follow its value;
+        // otherwise, default to generating the proof only if holder pub key is
+        // present.
+        auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account));
+        auto const contextHash =
+            getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq);
+
+        auto const proof = getSchnorrProof(*arg.account, contextHash);
+        if (proof)
+        {
+            jv[sfZKProof.jsonName] = strHex(*proof);
+        }
+        else
+        {
+            jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength));
+        }
+    }
+
+    auto const holderAmt = getBalance(*arg.account);
+    auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
+
+    auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+    auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+    auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+
+    if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
+        Throw("Failed to get Pre-convert balance");
+
+    std::optional prevAuditorBalance;
+    if (arg.auditorEncryptedAmt || auditor_)
+    {
+        prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
+        if (!prevAuditorBalance)
+            Throw("Failed to get Pre-convert balance");
+    }
+
+    auto const prevOutstanding = getIssuanceOutstandingBalance();
+
+    if (submit(arg, jv) == tesSUCCESS)
+    {
+        auto const postConfidentialOutstanding = getIssuanceConfidentialBalance();
+        auto const postOutstanding = getIssuanceOutstandingBalance();
+        env_.require(MptBalance(
+            *this, requireValue(arg.account, "account"), holderAmt - requireValue(arg.amt, "amt")));
+        env_.require(RequireAny([&]() -> bool {
+            return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
+        }));
+        env_.require(RequireAny([&]() -> bool {
+            return prevConfidentialOutstanding + *arg.amt == postConfidentialOutstanding;
+        }));
+
+        env_.require(RequireAny([&]() -> bool {
+            return getEncryptedBalance(*arg.account, holderEncryptedInbox).has_value();
+        }));
+        env_.require(RequireAny([&]() -> bool {
+            return getEncryptedBalance(*arg.account, holderEncryptedSpending).has_value();
+        }));
+        env_.require(RequireAny([&]() -> bool {
+            return getEncryptedBalance(*arg.account, issuerEncryptedBalance).has_value();
+        }));
+
+        auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+        auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+        auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+
+        if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
+            Throw("Failed to get post-convert balance");
+
+        if (arg.auditorEncryptedAmt || auditor_)
+        {
+            auto const postAuditorBalance =
+                getDecryptedBalance(*arg.account, auditorEncryptedBalance);
+
+            if (!postAuditorBalance)
+                Throw("Failed to get post-convert auditor balance");
+
+            env_.require(RequireAny([&]() -> bool {
+                return getEncryptedBalance(*arg.account, auditorEncryptedBalance).has_value();
+            }));
+
+            // auditor's encrypted balance is updated correctly
+            env_.require(RequireAny(
+                [&]() -> bool { return *prevAuditorBalance + *arg.amt == *postAuditorBalance; }));
+        }
+        // spending balance should not change
+        env_.require(
+            RequireAny([&]() -> bool { return *postSpendingBalance == *prevSpendingBalance; }));
+
+        // issuer's encrypted balance is updated correctly
+        env_.require(RequireAny(
+            [&]() -> bool { return *prevIssuerBalance + *arg.amt == *postIssuerBalance; }));
+
+        // holder's inbox balance is updated correctly
+        env_.require(RequireAny(
+            [&]() -> bool { return *prevInboxBalance + *arg.amt == *postInboxBalance; }));
+
+        // sum of holder's inbox and spending balance should equal to issuer's
+        // encrypted balance
+        env_.require(RequireAny([&]() -> bool {
+            return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
+        }));
+
+        if (arg.holderPubKey)
+        {
+            env_.require(RequireAny([&]() -> bool {
+                return forObject(
+                    [&](SLEP const& sle) -> bool {
+                        if (sle)
+                        {
+                            auto const holderPubKey = getPubKey(*arg.account);
+                            if (!holderPubKey)
+                            {
+                                Throw(
+                                    "MPTTester::convert: holder's pubkey is "
+                                    "not set");
+                            }
+
+                            return strHex((*sle)[sfHolderEncryptionKey]) == strHex(*holderPubKey);
+                        }
+                        return false;
+                    },
+                    arg.account);
+            }));
+        }
+    }
+}
+
+json::Value
+MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq)
+{
+    json::Value jv;
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+
+    jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    if (arg.amt)
+        jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
+    if (arg.holderPubKey)
+        jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey);
+
+    Buffer holderCiphertext;
+    Buffer issuerCiphertext;
+    std::optional auditorCiphertext;
+    Buffer blindingFactor;
+
+    fillConversionCiphertexts(
+        arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
+
+    jv[sfBlindingFactor.jsonName] = strHex(blindingFactor);
+
+    if (arg.proof)
+    {
+        jv[sfZKProof.jsonName] = *arg.proof;
+    }
+    else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value()))
+    {
+        auto const contextHash =
+            getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq);
+        auto const proof = getSchnorrProof(*arg.account, contextHash);
+        if (proof)
+        {
+            jv[sfZKProof.jsonName] = strHex(*proof);
+        }
+        else
+        {
+            jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength));
+        }
+    }
+
+    return jv;
+}
+
+void
+MPTTester::send(MPTConfidentialSend const& arg)
+{
+    json::Value jv;
+    jv[jss::TransactionType] = jss::ConfidentialMPTSend;
+
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+
+    if (arg.dest)
+    {
+        jv[sfDestination] = arg.dest->human();
+    }
+    else
+    {
+        Throw("Destination not specified");
+    }
+
+    if (!arg.amt)
+        Throw("Amount not specified for testing purposes");
+
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    Buffer const blindingFactor =
+        arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor();
+
+    // fill in the encrypted amounts if not provided
+    auto const senderAmt = arg.senderEncryptedAmt
+        ? *arg.senderEncryptedAmt
+        : encryptAmount(*arg.account, *arg.amt, blindingFactor);
+    auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt
+                                              : encryptAmount(*arg.dest, *arg.amt, blindingFactor);
+    auto const issuerAmt = arg.issuerEncryptedAmt
+        ? *arg.issuerEncryptedAmt
+        : encryptAmount(issuer_, *arg.amt, blindingFactor);
+
+    std::optional auditorAmt;
+    if (arg.auditorEncryptedAmt)
+    {
+        auditorAmt = arg.auditorEncryptedAmt;
+    }
+    else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false))
+    {
+        auditorAmt = encryptAmount(
+            requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor);
+    }
+
+    jv[sfSenderEncryptedAmount] = strHex(senderAmt);
+    jv[sfDestinationEncryptedAmount] = strHex(destAmt);
+    jv[sfIssuerEncryptedAmount] = strHex(issuerAmt);
+    if (auditorAmt)
+        jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt);
+
+    if (arg.credentials)
+    {
+        auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array);
+        for (auto const& hash : *arg.credentials)
+            arr.append(hash);
+    }
+
+    // Version counters before send
+    auto const prevSenderVersion = getMPTokenVersion(*arg.account);
+    auto const prevDestVersion = getMPTokenVersion(*arg.dest);
+
+    // Sender's previous confidential state
+    auto const prevSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+    auto const prevSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+    auto const prevSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+    auto const prevSenderInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox);
+    auto const prevSenderSpendingEncrypted =
+        getEncryptedBalance(*arg.account, holderEncryptedSpending);
+    auto const prevSenderIssuerEncrypted =
+        getEncryptedBalance(*arg.account, issuerEncryptedBalance);
+    if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer)
+        Throw("Failed to get Pre-send balance");
+
+    std::optional prevSenderAuditor;
+    auto const prevSenderAuditorEncrypted =
+        getEncryptedBalance(*arg.account, auditorEncryptedBalance);
+    if (arg.auditorEncryptedAmt || auditor_)
+    {
+        prevSenderAuditor = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
+        if (!prevSenderAuditor)
+            Throw("Failed to get Pre-send balance");
+    }
+
+    // Destination's previous confidential state
+    auto const prevDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox);
+    auto const prevDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending);
+    auto const prevDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance);
+    auto const prevDestInboxEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedInbox);
+    auto const prevDestSpendingEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedSpending);
+    auto const prevDestIssuerEncrypted = getEncryptedBalance(*arg.dest, issuerEncryptedBalance);
+    if (!prevDestInbox || !prevDestSpending || !prevDestIssuer)
+        Throw("Failed to get Pre-send balance");
+
+    std::optional prevDestAuditor;
+    auto const prevDestAuditorEncrypted = getEncryptedBalance(*arg.dest, auditorEncryptedBalance);
+    if (arg.auditorEncryptedAmt || auditor_)
+    {
+        prevDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance);
+        if (!prevDestAuditor)
+            Throw("Failed to get Pre-send balance");
+    }
+
+    // Fill in the commitment if not provided
+    // The amount commitment must use the same blinding factor as the ElGamal
+    // encryption. The sigma proof links the two, so using different randomness
+    // for each would cause proof verification to fail.
+    Buffer amountCommitment, balanceCommitment;
+    if (arg.amountCommitment)
+    {
+        amountCommitment = *arg.amountCommitment;
+    }
+    else
+    {
+        amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor);
+    }
+
+    jv[sfAmountCommitment] = strHex(amountCommitment);
+
+    auto const balanceBlindingFactor = generateBlindingFactor();
+    if (arg.balanceCommitment)
+    {
+        balanceCommitment = *arg.balanceCommitment;
+    }
+    else
+    {
+        balanceCommitment = getPedersenCommitment(*prevSenderSpending, balanceBlindingFactor);
+    }
+
+    jv[sfBalanceCommitment] = strHex(balanceCommitment);
+
+    // Fill in the proof if not provided
+    if (arg.proof)
+    {
+        jv[sfZKProof] = *arg.proof;
+    }
+    else
+    {
+        auto const version = getMPTokenVersion(*arg.account);
+        auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account));
+        auto const ctxHash = getSendContextHash(
+            requireValue(arg.account, "account").id(),
+            issuanceID(),
+            seq,
+            requireValue(arg.dest, "dest").id(),
+            version);
+
+        std::vector recipients;
+
+        auto const senderPubKey = getPubKey(*arg.account);
+        auto const destPubKey = getPubKey(*arg.dest);
+        auto const issuerPubKey = getPubKey(issuer_);
+
+        // If a key is missing, we skip adding the recipient. This intentionally
+        // causes proof generation to fail, triggering the dummy proof fallback.
+        if (senderPubKey)
+        {
+            recipients.push_back({
+                .publicKey = Slice(*senderPubKey),
+                .encryptedAmount = senderAmt,
+            });
+        }
+        if (destPubKey)
+        {
+            recipients.push_back({
+                .publicKey = Slice(*destPubKey),
+                .encryptedAmount = destAmt,
+            });
+        }
+        if (issuerPubKey)
+        {
+            recipients.push_back({
+                .publicKey = Slice(*issuerPubKey),
+                .encryptedAmount = issuerAmt,
+            });
+        }
+
+        std::optional auditorPubKey;
+        if (auditorAmt)
+        {
+            if (!auditor_)
+                Throw("Auditor not registered");
+
+            auditorPubKey = getPubKey(*auditor_);
+            if (auditorPubKey)
+            {
+                recipients.push_back({
+                    .publicKey = Slice(*auditorPubKey),
+                    .encryptedAmount = *auditorAmt,
+                });
+            }
+        }
+
+        std::optional proof;
+
+        // Skip proof generation if encrypted balance is missing (e.g.,
+        // feature disabled), when the sender and destination are the same
+        // (malformed case causing pcm to be zero), or when spending balance
+        // is 0
+        if (arg.account != arg.dest && prevSenderSpendingEncrypted && *prevSenderSpending > 0)
+        {
+            proof = getConfidentialSendProof(
+                *arg.account,
+                *arg.amt,
+                recipients,
+                blindingFactor,
+                ctxHash,
+                {
+                    .pedersenCommitment = amountCommitment,
+                    .amt = *arg.amt,
+                    .encryptedAmt = senderAmt,
+                    .blindingFactor = blindingFactor,
+                },
+                {
+                    .pedersenCommitment = balanceCommitment,
+                    .amt = *prevSenderSpending,
+                    .encryptedAmt = *prevSenderSpendingEncrypted,
+                    .blindingFactor = balanceBlindingFactor,
+                });
+        }
+
+        if (proof)
+        {
+            jv[sfZKProof.jsonName] = strHex(*proof);
+        }
+        else
+        {
+            jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength));
+        }
+    }
+
+    auto const senderPubAmt = getBalance(*arg.account);
+    auto const destPubAmt = getBalance(*arg.dest);
+    auto const prevCOA = getIssuanceConfidentialBalance();
+    auto const prevOA = getIssuanceOutstandingBalance();
+
+    if (submit(arg, jv) == tesSUCCESS)
+    {
+        auto const postCOA = getIssuanceConfidentialBalance();
+        auto const postOA = getIssuanceOutstandingBalance();
+
+        // Sender's post confidential state
+        auto const postSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+        auto const postSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+        auto const postSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+
+        if (!postSenderInbox || !postSenderSpending || !postSenderIssuer)
+            Throw("Failed to get Post-send balance");
+
+        // Destination's post confidential state
+        auto const postDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox);
+        auto const postDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending);
+        auto const postDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance);
+
+        if (!postDestInbox || !postDestSpending || !postDestIssuer)
+            Throw("Failed to get Post-send balance");
+
+        // Public balances unchanged
+        env_.require(MptBalance(*this, *arg.account, senderPubAmt));
+        env_.require(MptBalance(*this, *arg.dest, destPubAmt));
+
+        // OA and COA unchanged
+        env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }));
+        env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }));
+
+        // Verify sender changes
+        env_.require(RequireAny([&]() -> bool {
+            return *prevSenderSpending >= *arg.amt &&
+                *postSenderSpending == *prevSenderSpending - *arg.amt;
+        }));
+        env_.require(RequireAny([&]() -> bool { return postSenderInbox == prevSenderInbox; }));
+        env_.require(RequireAny([&]() -> bool {
+            return *prevSenderIssuer >= *arg.amt &&
+                *postSenderIssuer == *prevSenderIssuer - *arg.amt;
+        }));
+
+        // Verify destination changes
+        env_.require(
+            RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + *arg.amt; }));
+        env_.require(RequireAny([&]() -> bool { return *postDestSpending == *prevDestSpending; }));
+        env_.require(
+            RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + *arg.amt; }));
+
+        // Cross checks
+        env_.require(RequireAny(
+            [&]() -> bool { return *postSenderInbox + *postSenderSpending == *postSenderIssuer; }));
+        env_.require(RequireAny(
+            [&]() -> bool { return *postDestInbox + *postDestSpending == *postDestIssuer; }));
+
+        // Version: sender increments by 1; receiver version is unchanged by incoming sends
+        env_.require(RequireAny(
+            [&]() -> bool { return getMPTokenVersion(*arg.account) == prevSenderVersion + 1; }));
+        env_.require(
+            RequireAny([&]() -> bool { return getMPTokenVersion(*arg.dest) == prevDestVersion; }));
+
+        if (arg.auditorEncryptedAmt || auditor_)
+        {
+            auto const postSenderAuditor =
+                getDecryptedBalance(*arg.account, auditorEncryptedBalance);
+            auto const postDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance);
+            if (!postSenderAuditor || !postDestAuditor)
+                Throw("Failed to get Post-send balance");
+
+            env_.require(RequireAny([&]() -> bool {
+                return *postSenderAuditor == *postSenderIssuer &&
+                    *postDestAuditor == *postDestIssuer;
+            }));
+
+            // verify sender
+            env_.require(RequireAny([&]() -> bool {
+                return prevSenderAuditor >= *arg.amt &&
+                    *postSenderAuditor == *prevSenderAuditor - *arg.amt;
+            }));
+
+            // verify dest
+            env_.require(RequireAny(
+                [&]() -> bool { return *postDestAuditor == *prevDestAuditor + *arg.amt; }));
+        }
+    }
+}
+
+json::Value
+MPTTester::sendJV(
+    MPTConfidentialSend const& arg,
+    std::uint32_t seq,
+    std::optional chain)
+{
+    json::Value jv;
+    jv[jss::TransactionType] = jss::ConfidentialMPTSend;
+
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+
+    if (arg.dest)
+    {
+        jv[sfDestination] = arg.dest->human();
+    }
+    else
+    {
+        Throw("Destination not specified");
+    }
+
+    if (!arg.amt)
+        Throw("Amount not specified for testing purposes");
+
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    Buffer const blindingFactor =
+        arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor();
+
+    auto const senderAmt = arg.senderEncryptedAmt
+        ? *arg.senderEncryptedAmt
+        : encryptAmount(*arg.account, *arg.amt, blindingFactor);
+    auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt
+                                              : encryptAmount(*arg.dest, *arg.amt, blindingFactor);
+    auto const issuerAmt = arg.issuerEncryptedAmt
+        ? *arg.issuerEncryptedAmt
+        : encryptAmount(issuer_, *arg.amt, blindingFactor);
+
+    std::optional auditorAmt;
+    if (arg.auditorEncryptedAmt)
+    {
+        auditorAmt = arg.auditorEncryptedAmt;
+    }
+    else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false))
+    {
+        auditorAmt = encryptAmount(
+            requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor);
+    }
+
+    jv[sfSenderEncryptedAmount] = strHex(senderAmt);
+    jv[sfDestinationEncryptedAmount] = strHex(destAmt);
+    jv[sfIssuerEncryptedAmount] = strHex(issuerAmt);
+    if (auditorAmt)
+        jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt);
+
+    if (arg.credentials)
+    {
+        auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array);
+        for (auto const& hash : *arg.credentials)
+            arr.append(hash);
+    }
+
+    std::uint64_t prevSenderSpending = 0;
+    std::optional prevEncryptedSenderSpending;
+    std::uint32_t version = 0;
+    if (chain)
+    {
+        prevSenderSpending = chain->spending;
+        prevEncryptedSenderSpending = chain->encSpending;
+        version = chain->version;
+    }
+    else
+    {
+        auto const ledgerSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+        if (!ledgerSpending)
+            Throw("Failed to get sender spending balance");
+        prevSenderSpending = *ledgerSpending;
+        prevEncryptedSenderSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending);
+        version = getMPTokenVersion(*arg.account);
+    }
+
+    // The amount commitment must use the same blinding factor as the tx ElGamal
+    // encryption blinding factor.
+    Buffer amountCommitment, balanceCommitment;
+    if (arg.amountCommitment)
+    {
+        amountCommitment = *arg.amountCommitment;
+    }
+    else
+    {
+        amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor);
+    }
+
+    jv[sfAmountCommitment] = strHex(amountCommitment);
+
+    auto const balanceBlindingFactor = generateBlindingFactor();
+    if (arg.balanceCommitment)
+    {
+        balanceCommitment = *arg.balanceCommitment;
+    }
+    else
+    {
+        balanceCommitment = getPedersenCommitment(prevSenderSpending, balanceBlindingFactor);
+    }
+
+    jv[sfBalanceCommitment] = strHex(balanceCommitment);
+
+    if (arg.proof)
+    {
+        jv[sfZKProof.jsonName] = *arg.proof;
+    }
+    else
+    {
+        auto const ctxHash = getSendContextHash(
+            requireValue(arg.account, "account").id(),
+            issuanceID(),
+            seq,
+            requireValue(arg.dest, "dest").id(),
+            version);
+
+        std::vector recipients;
+
+        auto const senderPubKey = getPubKey(*arg.account);
+        auto const destPubKey = getPubKey(*arg.dest);
+        auto const issuerPubKey = getPubKey(issuer_);
+
+        if (senderPubKey)
+        {
+            recipients.push_back({
+                .publicKey = Slice(*senderPubKey),
+                .encryptedAmount = senderAmt,
+            });
+        }
+        if (destPubKey)
+        {
+            recipients.push_back({
+                .publicKey = Slice(*destPubKey),
+                .encryptedAmount = destAmt,
+            });
+        }
+        if (issuerPubKey)
+        {
+            recipients.push_back({
+                .publicKey = Slice(*issuerPubKey),
+                .encryptedAmount = issuerAmt,
+            });
+        }
+
+        std::optional auditorPubKey;
+        if (auditorAmt)
+        {
+            if (!auditor_)
+                Throw("Auditor not registered");
+            auditorPubKey = getPubKey(*auditor_);
+            if (auditorPubKey)
+            {
+                recipients.push_back({
+                    .publicKey = Slice(*auditorPubKey),
+                    .encryptedAmount = *auditorAmt,
+                });
+            }
+        }
+
+        std::optional proof;
+
+        // Skip proof generation when spending balance is 0
+        if (arg.account != arg.dest && prevEncryptedSenderSpending && prevSenderSpending > 0)
+        {
+            proof = getConfidentialSendProof(
+                *arg.account,
+                *arg.amt,
+                recipients,
+                blindingFactor,
+                ctxHash,
+                {
+                    .pedersenCommitment = amountCommitment,
+                    .amt = *arg.amt,
+                    .encryptedAmt = senderAmt,
+                    .blindingFactor = blindingFactor,
+                },
+                {
+                    .pedersenCommitment = balanceCommitment,
+                    .amt = prevSenderSpending,
+                    .encryptedAmt = *prevEncryptedSenderSpending,
+                    .blindingFactor = balanceBlindingFactor,
+                });
+        }
+
+        if (proof)
+        {
+            jv[sfZKProof.jsonName] = strHex(*proof);
+        }
+        else
+        {
+            jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength));
+        }
+    }
+
+    return jv;
+}
+
+static Buffer
+parseSenderEncAmt(json::Value const& jv)
+{
+    auto const hexStr = jv[sfSenderEncryptedAmount.jsonName].asString();
+    auto const bytes = strUnHex(hexStr);
+    if (!bytes)
+        Throw("chainAfterSend: invalid hex in sfSenderEncryptedAmount");
+    return Buffer(bytes->data(), bytes->size());
+}
+
+ConfidentialSendChainState
+MPTTester::chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const
+{
+    auto const prevSpending = getDecryptedBalance(sender, holderEncryptedSpending);
+    auto const prevEncSpending = getEncryptedBalance(sender, holderEncryptedSpending);
+    auto const prevVersion = getMPTokenVersion(sender);
+
+    if (!prevSpending || !prevEncSpending)
+        Throw("chainAfterSend: failed to read sender state from ledger");
+
+    Buffer const senderEncAmt = parseSenderEncAmt(jv);
+    auto chain = computeNextSendChainState(
+        *prevSpending, Slice(*prevEncSpending), prevVersion, sendAmt, Slice(senderEncAmt));
+    if (!chain)
+        Throw("chainAfterSend: computeNextSendChainState failed");
+    return std::move(*chain);
+}
+
+std::optional
+computeNextSendChainState(
+    std::uint64_t currentSpending,
+    Slice const& currentEncSpending,
+    std::uint32_t currentVersion,
+    std::uint64_t sendAmt,
+    Slice const& senderEncAmt)
+{
+    if (sendAmt > currentSpending)
+        return std::nullopt;  // LCOV_EXCL_LINE
+
+    auto newEncSpending = homomorphicSubtract(currentEncSpending, senderEncAmt);
+    if (!newEncSpending)
+        return std::nullopt;  // LCOV_EXCL_LINE
+
+    return ConfidentialSendChainState{
+        .spending = currentSpending - sendAmt,
+        .encSpending = std::move(*newEncSpending),
+        .version = currentVersion + 1,
+    };
+}
+
+void
+MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
+{
+    json::Value jv;
+    auto const account = arg.account ? *arg.account : issuer_;
+    jv[sfAccount] = account.human();
+
+    if (arg.holder)
+    {
+        jv[sfHolder] = arg.holder->human();
+    }
+    else
+    {
+        Throw("Holder not specified");
+    }
+
+    jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else if (id_)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+    else
+    {
+        Throw("MPT has not been created");
+    }
+
+    if (arg.amt)
+        jv[sfMPTAmount] = std::to_string(*arg.amt);
+
+    if (arg.proof)
+    {
+        jv[sfZKProof] = *arg.proof;
+    }
+    else
+    {
+        auto const seq = arg.ticketSeq ? *arg.ticketSeq : env_.seq(account);
+        auto const contextHash = getClawbackContextHash(
+            account.id(), issuanceID(), seq, requireValue(arg.holder, "holder").id());
+
+        auto const privKey = getPrivKey(account);
+        if (!privKey || privKey->size() != kEcPrivKeyLength)
+            Throw("Failed to get clawback private key");
+
+        auto const proof = getClawbackProof(
+            requireValue(arg.holder, "holder"),
+            requireValue(arg.amt, "amt"),
+            requireValue(privKey, "privKey"),
+            contextHash);
+
+        if (proof)
+        {
+            jv[sfZKProof] = strHex(*proof);
+        }
+        else
+        {
+            jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcClawbackProofLength));
+        }
+    }
+
+    auto const holderPubAmt = getBalance(*arg.holder);
+    auto const prevCOA = getIssuanceConfidentialBalance();
+    auto const prevOA = getIssuanceOutstandingBalance();
+    auto const prevVersion = getMPTokenVersion(*arg.holder);
+
+    if (submit(arg, jv) == tesSUCCESS)
+    {
+        auto const postCOA = getIssuanceConfidentialBalance();
+        auto const postOA = getIssuanceOutstandingBalance();
+        auto const postVersion = getMPTokenVersion(*arg.holder);
+
+        // Verify holder's public balance is unchanged
+        env_.require(MptBalance(*this, *arg.holder, holderPubAmt));
+
+        // Verify COA and OA are reduced correctly
+        env_.require(RequireAny(
+            [&]() -> bool { return prevCOA >= *arg.amt && postCOA == prevCOA - *arg.amt; }));
+        env_.require(RequireAny([&]() -> bool {
+            return prevOA && postOA && *prevOA >= *arg.amt && *postOA == *prevOA - *arg.amt;
+        }));
+
+        // Verify holder's confidential balances are zeroed out
+        env_.require(RequireAny(
+            [&]() -> bool { return getDecryptedBalance(*arg.holder, holderEncryptedInbox) == 0; }));
+        env_.require(RequireAny([&]() -> bool {
+            return getDecryptedBalance(*arg.holder, holderEncryptedSpending) == 0;
+        }));
+        env_.require(RequireAny([&]() -> bool {
+            return getDecryptedBalance(*arg.holder, issuerEncryptedBalance) == 0;
+        }));
+        env_.require(RequireAny([&]() -> bool {
+            return getDecryptedBalance(*arg.holder, auditorEncryptedBalance) == 0;
+        }));
+
+        // Verify version is incremented
+        env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
+    }
+}
+
+void
+MPTTester::generateKeyPair(Account const& account)
+{
+    unsigned char privKey[kEcPrivKeyLength];
+    secp256k1_pubkey pubKey;
+    if (secp256k1_elgamal_generate_keypair(secp256k1Context(), privKey, &pubKey) == 0)
+        Throw("failed to generate key pair");
+
+    // Serialize public key to compressed format (33 bytes)
+    unsigned char compressedPubKey[kEcPubKeyLength];
+    size_t outLen = kEcPubKeyLength;
+    if (secp256k1_ec_pubkey_serialize(
+            secp256k1Context(), compressedPubKey, &outLen, &pubKey, SECP256K1_EC_COMPRESSED) != 1 ||
+        outLen != kEcPubKeyLength)
+    {
+        Throw("failed to serialize public key");
+    }
+
+    pubKeys_.insert({account.id(), Buffer{compressedPubKey, kEcPubKeyLength}});
+    privKeys_.insert({account.id(), Buffer{privKey, kEcPrivKeyLength}});
+}
+
+std::optional
+MPTTester::getPubKey(Account const& account) const
+{
+    if (auto const it = pubKeys_.find(account.id()); it != pubKeys_.end())
+        return it->second;
+
+    return std::nullopt;
+}
+
+std::optional
+MPTTester::getPrivKey(Account const& account) const
+{
+    if (auto const it = privKeys_.find(account.id()); it != privKeys_.end())
+        return it->second;
+
+    return std::nullopt;
+}
+
+Buffer
+MPTTester::encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor)
+    const
+{
+    if (auto const pubKey = getPubKey(account))
+    {
+        if (auto const result = xrpl::encryptAmount(amt, *pubKey, blindingFactor))
+            return *result;
+    }
+
+    // Return a dummy buffer on failure to allow testing of
+    // failures that occur prior to encryption.
+    return gMakeZeroBuffer(kEcGamalEncryptedTotalLength);
+}
+
+std::optional
+MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
+{
+    if (amt.size() != kEcGamalEncryptedTotalLength)
+        return std::nullopt;
+
+    auto const pair = makeEcPair(amt);
+    if (!pair)
+        return std::nullopt;
+
+    auto const privKey = getPrivKey(account);
+    if (!privKey || privKey->size() != kEcPrivKeyLength)
+        return std::nullopt;
+
+    uint64_t decryptedAmt = 0;
+    if (secp256k1_elgamal_decrypt(
+            secp256k1Context(),
+            &decryptedAmt,
+            &pair->c1,
+            &pair->c2,
+            privKey->data(),
+            kElGamalDecryptRangeLow,
+            kElGamalDecryptRangeHigh) == 0)
+    {
+        return std::nullopt;
+    }
+
+    return decryptedAmt;
+}
+
+std::optional
+MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const
+{
+    auto const encryptedAmt = getEncryptedBalance(account, balanceType);
+
+    // Return zero to test cases like Feature Disabled, where the ledger object
+    // does not exist.
+    if (!encryptedAmt)
+        return 0;
+
+    Account decryptor = account;
+
+    if (balanceType == issuerEncryptedBalance)
+    {
+        decryptor = issuer_;
+    }
+    else if (balanceType == auditorEncryptedBalance)
+    {
+        if (!auditor_)
+            return std::nullopt;
+        decryptor = *auditor_;
+    }
+
+    return decryptAmount(decryptor, *encryptedAmt);
+};
+
+json::Value
+MPTTester::mergeInboxJV(MPTMergeInbox const& arg) const
+{
+    json::Value jv;
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+    jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox;
+    return jv;
+}
+
+void
+MPTTester::mergeInbox(MPTMergeInbox const& arg)
+{
+    json::Value jv;
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox;
+    auto const holderPubAmt = getBalance(*arg.account);
+    auto const prevCOA = getIssuanceConfidentialBalance();
+    auto const prevOA = getIssuanceOutstandingBalance();
+    auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+    auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+    auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+    auto const prevIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance);
+    auto const prevAuditorEncrypted = getEncryptedBalance(*arg.account, auditorEncryptedBalance);
+    auto const prevVersion = getMPTokenVersion(*arg.account);
+
+    if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
+        Throw("Failed to get pre-mergeInbox balances");
+
+    if (submit(arg, jv) == tesSUCCESS)
+    {
+        auto const postCOA = getIssuanceConfidentialBalance();
+        auto const postOA = getIssuanceOutstandingBalance();
+        auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+        auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+        auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+        auto const postInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox);
+        auto const postIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance);
+        auto const postAuditorEncrypted =
+            getEncryptedBalance(*arg.account, auditorEncryptedBalance);
+        auto const postVersion = getMPTokenVersion(*arg.account);
+
+        if (!postInboxBalance || !postSpendingBalance || !postIssuerBalance ||
+            !prevIssuerEncrypted || !postInboxEncrypted || !postIssuerEncrypted)
+            Throw("Failed to get post-mergeInbox balances");
+
+        env_.require(MptBalance(*this, *arg.account, holderPubAmt));
+        env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }));
+        env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }));
+
+        env_.require(RequireAny([&]() -> bool {
+            return *postSpendingBalance == *prevInboxBalance + *prevSpendingBalance &&
+                *postInboxBalance == 0;
+        }));
+
+        env_.require(
+            RequireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; }));
+
+        auto const holderPubKey = getPubKey(*arg.account);
+        if (!holderPubKey)
+            Throw("Failed to get holder public key");
+
+        auto const expectedInbox = encryptCanonicalZeroAmount(
+            requireValue(holderPubKey, "holderPubKey"),
+            requireValue(arg.account, "account").id(),
+            issuanceID());
+        if (!expectedInbox)
+            Throw("Failed to get canonical zero encryption");
+
+        env_.require(RequireAny([&]() -> bool { return *postInboxEncrypted == *expectedInbox; }));
+        env_.require(
+            RequireAny([&]() -> bool { return *postIssuerEncrypted == *prevIssuerEncrypted; }));
+        env_.require(RequireAny([&]() -> bool {
+            return postAuditorEncrypted.has_value() == prevAuditorEncrypted.has_value() &&
+                (!postAuditorEncrypted || *postAuditorEncrypted == *prevAuditorEncrypted);
+        }));
+        env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
+
+        env_.require(RequireAny([&]() -> bool {
+            return *postSpendingBalance + *postInboxBalance == *postIssuerBalance;
+        }));
+    }
+}
+
+std::optional
+MPTTester::getIssuanceOutstandingBalance() const
+{
+    if (!id_)
+        return std::nullopt;
+
+    auto const sle = env_.current()->read(keylet::mptokenIssuance(*id_));
+
+    if (!sle)
+        return std::nullopt;
+
+    return (*sle)[sfOutstandingAmount];
+}
+
+std::uint32_t
+MPTTester::getMPTokenVersion(Account const account) const
+{
+    if (!id_)
+        Throw("Issuance ID does not exist");
+
+    auto const sle = env_.current()->read(keylet::mptoken(*id_, account));
+
+    // return 0 here instead of throwing an exception since tests for
+    // preclaim will check if the MPToken exists
+    if (!sle)
+        return 0;
+
+    return (*sle)[~sfConfidentialBalanceVersion].value_or(0);
+}
+
+void
+MPTTester::convertBack(MPTConvertBack const& arg)
+{
+    json::Value jv;
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+
+    jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack;
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    if (arg.amt)
+        jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
+
+    Buffer holderCiphertext;
+    Buffer issuerCiphertext;
+    std::optional auditorCiphertext;
+    Buffer blindingFactor;
+
+    fillConversionCiphertexts(
+        arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
+
+    jv[sfBlindingFactor] = strHex(blindingFactor);
+
+    auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+    auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+    auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+
+    if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
+        Throw("Failed to get Pre-convertBack balance");
+
+    Buffer pedersenCommitment;
+    Buffer const pcBlindingFactor = generateBlindingFactor();
+    if (arg.pedersenCommitment)
+    {
+        pedersenCommitment = *arg.pedersenCommitment;
+    }
+    else
+    {
+        pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor);
+    }
+
+    jv[sfBalanceCommitment] = strHex(pedersenCommitment);
+
+    if (arg.proof)
+    {
+        jv[sfZKProof.jsonName] = strHex(*arg.proof);
+    }
+    else
+    {
+        auto const version = getMPTokenVersion(*arg.account);
+
+        // if the caller generated ciphertexts themselves, they should also
+        // generate the proof themselves from the blinding factor
+        auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account));
+        auto const contextHash = getConvertBackContextHash(
+            requireValue(arg.account, "account").id(), issuanceID(), seq, version);
+        auto const prevEncryptedSpendingBalance =
+            getEncryptedBalance(*arg.account, holderEncryptedSpending);
+
+        Buffer proof;
+        // generate a dummy proof if no encrypted amount field, so that other
+        // preflight/preclaim are checked
+        if (!prevEncryptedSpendingBalance)
+        {
+            proof = gMakeZeroBuffer(kEcConvertBackProofLength);
+        }
+        else
+        {
+            proof = getConvertBackProof(
+                *arg.account,
+                requireValue(arg.amt, "amt"),
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = *prevSpendingBalance,
+                    .encryptedAmt = *prevEncryptedSpendingBalance,
+                    .blindingFactor = pcBlindingFactor,
+                });
+        }
+        jv[sfZKProof] = strHex(proof);
+    }
+
+    auto const holderAmt = getBalance(*arg.account);
+    auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
+
+    std::optional prevAuditorBalance;
+    if (arg.auditorEncryptedAmt || auditor_)
+    {
+        prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
+        if (!prevAuditorBalance)
+            Throw("Failed to get Pre-convertBack balance");
+    }
+
+    auto const prevOutstanding = getIssuanceOutstandingBalance();
+    auto const prevVersion = getMPTokenVersion(*arg.account);
+
+    if (submit(arg, jv) == tesSUCCESS)
+    {
+        auto const postConfidentialOutstanding = getIssuanceConfidentialBalance();
+        auto const postOutstanding = getIssuanceOutstandingBalance();
+        auto const postVersion = getMPTokenVersion(*arg.account);
+        env_.require(MptBalance(
+            *this, requireValue(arg.account, "account"), holderAmt + requireValue(arg.amt, "amt")));
+        env_.require(RequireAny([&]() -> bool {
+            return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
+        }));
+        env_.require(RequireAny([&]() -> bool {
+            return prevConfidentialOutstanding - *arg.amt == postConfidentialOutstanding;
+        }));
+
+        auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
+        auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
+        auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+
+        if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
+            Throw("Failed to get post-convertBack balance");
+
+        if (arg.auditorEncryptedAmt || auditor_)
+        {
+            auto const postAuditorBalance =
+                getDecryptedBalance(*arg.account, auditorEncryptedBalance);
+
+            if (!postAuditorBalance)
+                Throw("Failed to get post-convertBack balance");
+
+            // auditor's encrypted balance is updated correctly
+            env_.require(RequireAny(
+                [&]() -> bool { return *prevAuditorBalance - *arg.amt == *postAuditorBalance; }));
+        }
+
+        // inbox balance should not change
+        env_.require(RequireAny([&]() -> bool { return *postInboxBalance == *prevInboxBalance; }));
+
+        // issuer's encrypted balance is updated correctly
+        env_.require(RequireAny(
+            [&]() -> bool { return *prevIssuerBalance - *arg.amt == *postIssuerBalance; }));
+
+        // holder's spending balance is updated correctly
+        env_.require(RequireAny(
+            [&]() -> bool { return *prevSpendingBalance - *arg.amt == *postSpendingBalance; }));
+
+        // holder's confidential balance version is updated correctly
+        env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
+
+        // sum of holder's inbox and spending balance should equal to issuer's
+        // encrypted balance
+        env_.require(RequireAny([&]() -> bool {
+            return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
+        }));
+    }
+}
+
+json::Value
+MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq)
+{
+    json::Value jv;
+    if (arg.account)
+    {
+        jv[sfAccount] = arg.account->human();
+    }
+    else
+    {
+        Throw("Account not specified");
+    }
+
+    jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack;
+    if (arg.id)
+    {
+        jv[sfMPTokenIssuanceID] = to_string(*arg.id);
+    }
+    else
+    {
+        if (!id_)
+            Throw("MPT has not been created");
+        jv[sfMPTokenIssuanceID] = to_string(*id_);
+    }
+
+    if (arg.amt)
+        jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
+
+    Buffer holderCiphertext;
+    Buffer issuerCiphertext;
+    std::optional auditorCiphertext;
+    Buffer blindingFactor;
+
+    fillConversionCiphertexts(
+        arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
+
+    jv[sfBlindingFactor] = strHex(blindingFactor);
+
+    auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
+    if (!prevSpendingBalance)
+        Throw("convertBackJV: failed to read spending balance from ledger");
+
+    Buffer pedersenCommitment;
+    Buffer const pcBlindingFactor = generateBlindingFactor();
+    if (arg.pedersenCommitment)
+    {
+        pedersenCommitment = *arg.pedersenCommitment;
+    }
+    else
+    {
+        pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor);
+    }
+
+    jv[sfBalanceCommitment] = strHex(pedersenCommitment);
+
+    if (arg.proof)
+    {
+        jv[sfZKProof.jsonName] = strHex(*arg.proof);
+    }
+    else
+    {
+        auto const version = getMPTokenVersion(*arg.account);
+        auto const prevEncSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending);
+        auto const contextHash = getConvertBackContextHash(
+            requireValue(arg.account, "account").id(), issuanceID(), seq, version);
+
+        Buffer proof;
+        if (!prevEncSpending)
+        {
+            proof = gMakeZeroBuffer(kEcConvertBackProofLength);
+        }
+        else
+        {
+            proof = getConvertBackProof(
+                *arg.account,
+                requireValue(arg.amt, "amt"),
+                contextHash,
+                {
+                    .pedersenCommitment = pedersenCommitment,
+                    .amt = *prevSpendingBalance,
+                    .encryptedAmt = *prevEncSpending,
+                    .blindingFactor = pcBlindingFactor,
+                });
+        }
+
+        jv[sfZKProof] = strHex(proof);
+    }
+
+    return jv;
+}
+
 }  // namespace xrpl::test::jtx
diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp
index 7da419c6e7..c298cee684 100644
--- a/src/test/jtx/impl/utility.cpp
+++ b/src/test/jtx/impl/utility.cpp
@@ -13,6 +13,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -57,7 +58,22 @@ fillFee(json::Value& jv, ReadView const& view)
 {
     if (jv.isMember(jss::Fee))
         return;
-    jv[jss::Fee] = to_string(view.fees().base);
+
+    auto const base = view.fees().base;
+
+    // For confidential transactions, the fee is higher because confidential
+    // transaction processing is more expensive.
+    auto const txType = jv[jss::TransactionType].asString();
+    if (txType == jss::ConfidentialMPTConvert || txType == jss::ConfidentialMPTConvertBack ||
+        txType == jss::ConfidentialMPTSend || txType == jss::ConfidentialMPTMergeInbox ||
+        txType == jss::ConfidentialMPTClawback)
+    {
+        jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1));
+    }
+    else
+    {
+        jv[jss::Fee] = to_string(base);
+    }
 }
 
 void
diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h
index c8a65d7541..d5fba82e08 100644
--- a/src/test/jtx/mpt.h
+++ b/src/test/jtx/mpt.h
@@ -2,12 +2,21 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl::test::jtx {
 
@@ -15,7 +24,28 @@ class MPTTester;
 
 auto const kMptDexFlags = tfMPTCanTrade | tfMPTCanTransfer;
 
-// Check flags settings on MPT create
+/**
+ * @brief Create a zero-initialized buffer for malformed cryptography test
+ * inputs.
+ *
+ * xrpl::Buffer(size) allocates uninitialized heap memory. Because CI runs unit
+ * tests sequentially in the same process, uninitialized memory can recycle
+ * valid secp256k1 keys or Pedersen commitments from earlier tests. Explicitly
+ * zeroing the buffer guarantees structural validation fails deterministically.
+ *
+ * @param size The number of zero bytes to allocate.
+ * @return A buffer containing size zero bytes.
+ */
+[[nodiscard]] inline Buffer
+gMakeZeroBuffer(std::size_t size)
+{
+    Buffer b(size);
+    if (size > 0)
+        std::memset(b.data(), 0, size);
+    return b;
+}
+
+/** @brief Test helper that checks MPT flag settings after creation. */
 class MptFlags
 {
 private:
@@ -36,7 +66,7 @@ public:
     operator()(Env& env) const;
 };
 
-// Check mptissuance or mptoken amount balances on payment
+/** @brief Test helper that checks MPT issuance or holder balances. */
 class MptBalance
 {
 private:
@@ -54,6 +84,7 @@ public:
     operator()(Env& env) const;
 };
 
+/** @brief Test helper that accepts any condition supplied by a callback. */
 class RequireAny
 {
 private:
@@ -70,6 +101,7 @@ public:
 
 using Holders = std::vector;
 
+/** @brief Arguments for building an MPTokenIssuanceCreate test transaction. */
 struct MPTCreate
 {
     static inline std::vector allHolders = {};
@@ -93,9 +125,13 @@ struct MPTCreate
     std::optional err = std::nullopt;
 };
 
+/** @brief Arguments for initializing funded MPT test accounts and issuance. */
 struct MPTInit
 {
+    // Default-initialized so designated-initializer call sites that omit
+    // `holders` don't trip GCC's -Werror=missing-field-initializers.
     Holders holders = {};  // NOLINT(readability-redundant-member-init)
+    std::optional auditor = std::nullopt;
     PrettyAmount const xrp = XRP(10'000);
     PrettyAmount const xrpHolders = XRP(10'000);
     bool fund = true;
@@ -105,11 +141,13 @@ struct MPTInit
 };
 static MPTInit const kMptInitNoFund{.fund = false};
 
+/** @brief Full constructor arguments for MPTTester initialization. */
 struct MPTInitDef
 {
     Env& env;
     Account issuer;
     Holders holders = {};  // NOLINT(readability-redundant-member-init)
+    std::optional auditor = std::nullopt;
     std::uint16_t transferFee = 0;
     std::optional pay = std::nullopt;
     std::uint32_t flags = kMptDexFlags;
@@ -121,6 +159,7 @@ struct MPTInitDef
     std::optional err = std::nullopt;
 };
 
+/** @brief Arguments for building an MPTokenIssuanceDestroy test transaction. */
 struct MPTDestroy
 {
     std::optional issuer = std::nullopt;
@@ -131,6 +170,7 @@ struct MPTDestroy
     std::optional err = std::nullopt;
 };
 
+/** @brief Arguments for building an MPTokenAuthorize test transaction. */
 struct MPTAuthorize
 {
     std::optional account = std::nullopt;
@@ -142,6 +182,7 @@ struct MPTAuthorize
     std::optional err = std::nullopt;
 };
 
+/** @brief Arguments for building an MPTokenIssuanceSet test transaction. */
 struct MPTSet
 {
     std::optional account = std::nullopt;
@@ -155,18 +196,215 @@ struct MPTSet
     std::optional metadata = std::nullopt;
     std::optional delegate = std::nullopt;
     std::optional domainID = std::nullopt;
+    std::optional issuerPubKey = std::nullopt;
+    std::optional auditorPubKey = std::nullopt;
+    std::optional ticketSeq = std::nullopt;
     std::optional err = std::nullopt;
 };
 
+/** @brief Arguments for building a ConfidentialMPTConvert test transaction. */
+struct MPTConvert
+{
+    std::optional account = std::nullopt;
+    std::optional id = std::nullopt;
+    std::optional amt = std::nullopt;
+    std::optional proof = std::nullopt;
+    std::optional fillAuditorEncryptedAmt = true;
+    // indicates whether to autofill schnorr proof.
+    // default : auto generate proof if holderPubKey is present.
+    // true: force proof generation.
+    // false: force proof omission.
+    std::optional fillSchnorrProof = std::nullopt;
+    std::optional holderPubKey = std::nullopt;
+    std::optional holderEncryptedAmt = std::nullopt;
+    std::optional issuerEncryptedAmt = std::nullopt;
+    std::optional auditorEncryptedAmt = std::nullopt;
+
+    std::optional blindingFactor = std::nullopt;
+    std::optional delegate = std::nullopt;
+    std::optional ticketSeq = std::nullopt;
+    std::optional ownerCount = std::nullopt;
+    std::optional holderCount = std::nullopt;
+    std::optional flags = std::nullopt;
+    std::optional fee = std::nullopt;
+    std::optional err = std::nullopt;
+};
+
+/** @brief Arguments for building a ConfidentialMPTMergeInbox test transaction. */
+struct MPTMergeInbox
+{
+    std::optional account = std::nullopt;
+    std::optional id = std::nullopt;
+    std::optional delegate = std::nullopt;
+    std::optional ticketSeq = std::nullopt;
+    std::optional ownerCount = std::nullopt;
+    std::optional holderCount = std::nullopt;
+    std::optional flags = std::nullopt;
+    std::optional fee = std::nullopt;
+    std::optional err = std::nullopt;
+};
+
+/** @brief Arguments for building a ConfidentialMPTSend test transaction. */
+struct MPTConfidentialSend
+{
+    std::optional account = std::nullopt;
+    std::optional dest = std::nullopt;
+    std::optional id = std::nullopt;
+    // amt is to generate encrypted amounts for testing purposes
+    std::optional amt = std::nullopt;
+    std::optional proof = std::nullopt;
+    std::optional senderEncryptedAmt = std::nullopt;
+    std::optional destEncryptedAmt = std::nullopt;
+    std::optional issuerEncryptedAmt = std::nullopt;
+    std::optional auditorEncryptedAmt = std::nullopt;
+    std::optional fillAuditorEncryptedAmt = true;
+    std::optional> credentials = std::nullopt;
+    // not an txn param, only used for autofilling
+    std::optional blindingFactor = std::nullopt;
+    std::optional amountCommitment = std::nullopt;
+    std::optional balanceCommitment = std::nullopt;
+    std::optional delegate = std::nullopt;
+    std::optional destinationTag = std::nullopt;
+    std::optional ticketSeq = std::nullopt;
+    std::optional ownerCount = std::nullopt;
+    std::optional holderCount = std::nullopt;
+    std::optional flags = std::nullopt;
+    std::optional fee = std::nullopt;
+    std::optional err = std::nullopt;
+};
+
+/** @brief Arguments for building a ConfidentialMPTConvertBack test transaction. */
+struct MPTConvertBack
+{
+    std::optional account = std::nullopt;
+    std::optional id = std::nullopt;
+    std::optional amt = std::nullopt;
+    std::optional proof = std::nullopt;
+    std::optional holderEncryptedAmt = std::nullopt;
+    std::optional issuerEncryptedAmt = std::nullopt;
+    std::optional auditorEncryptedAmt = std::nullopt;
+    std::optional fillAuditorEncryptedAmt = true;
+    // not an txn param, only used for autofilling
+    std::optional blindingFactor = std::nullopt;
+    std::optional pedersenCommitment = std::nullopt;
+    std::optional delegate = std::nullopt;
+    std::optional ticketSeq = std::nullopt;
+    std::optional ownerCount = std::nullopt;
+    std::optional holderCount = std::nullopt;
+    std::optional flags = std::nullopt;
+    std::optional fee = std::nullopt;
+    std::optional err = std::nullopt;
+};
+
+/** @brief Arguments for building a ConfidentialMPTClawback test transaction. */
+struct MPTConfidentialClawback
+{
+    std::optional account = std::nullopt;
+    std::optional holder = std::nullopt;
+    std::optional id = std::nullopt;
+    std::optional amt = std::nullopt;
+    std::optional proof = std::nullopt;
+    std::optional delegate = std::nullopt;
+    std::optional ticketSeq = std::nullopt;
+    std::optional ownerCount = std::nullopt;
+    std::optional holderCount = std::nullopt;
+    std::optional flags = std::nullopt;
+    std::optional fee = std::nullopt;
+    std::optional err = std::nullopt;
+};
+
+/**
+ * @brief Stores the parameters that are exclusively used to generate a
+ * Pedersen linkage proof.
+ */
+struct PedersenProofParams
+{
+    /** @brief The Pedersen commitment used by the proof. */
+    Buffer const pedersenCommitment;
+
+    /** @brief Either the spending balance or the value being transferred. */
+    uint64_t const amt;
+
+    /** @brief The encrypted amount linked to the Pedersen commitment. */
+    Buffer const encryptedAmt;
+
+    /** @brief The blinding factor used to create the Pedersen commitment. */
+    Buffer const blindingFactor;
+};
+
+/**
+ * @brief When building multiple confidential sends from the same account inside a
+ * single batch transaction, pass this state to the transaction builder for
+ * each subsequent send so that its proof references the post previous-send
+ * encrypted balance rather than the stale pre-send ledger state.
+ *
+ * The fields mirror what the ledger will contain after the preceding send's
+ * doApply() completes:
+ *   encSpending = homomorphicSubtract(prevEncSpending, senderEncAmt)
+ *   version     = prevVersion + 1
+ */
+struct ConfidentialSendChainState
+{
+    /** @brief Decrypted spending balance after the previous send. */
+    std::uint64_t spending;
+
+    /** @brief Encrypted spending balance after the previous send. */
+    Buffer encSpending;
+
+    /** @brief sfConfidentialBalanceVersion after the previous send. */
+    std::uint32_t version;
+};
+
+/**
+ * @brief Use this when building a second (or later) confidential send from the same
+ * account in the same batch. Pass the state to the chain aware
+ * transaction builder so that the next proof is constructed against the
+ * correct post-send encrypted balance and version.
+ *
+ * @param currentSpending     Decrypted spending balance before the send.
+ * @param currentEncSpending  sfConfidentialBalanceSpending before the send.
+ * @param currentVersion      sfConfidentialBalanceVersion before the send.
+ * @param sendAmt             Plaintext amount being sent.
+ * @param senderEncAmt        sfSenderEncryptedAmount from the send transaction.
+ * @return The predicted chain state, or std::nullopt if homomorphic
+ *         subtraction fails.
+ */
+std::optional
+computeNextSendChainState(
+    std::uint64_t currentSpending,
+    Slice const& currentEncSpending,
+    std::uint32_t currentVersion,
+    std::uint64_t sendAmt,
+    Slice const& senderEncAmt);
+
+/**
+ * @brief Test helper for creating, mutating, and asserting MPT and confidential
+ * MPT ledger state.
+ */
 class MPTTester
 {
     Env& env_;
     Account const issuer_;
     std::unordered_map const holders_;
+    std::optional const auditor_;
     std::optional id_;
     bool close_;
+    std::unordered_map pubKeys_;
+    std::unordered_map privKeys_;
 
 public:
+    enum class EncryptedBalanceType {
+        IssuerEncryptedBalance,
+        HolderEncryptedInbox,
+        HolderEncryptedSpending,
+        AuditorEncryptedBalance,
+    };
+
+    static constexpr auto issuerEncryptedBalance = EncryptedBalanceType::IssuerEncryptedBalance;
+    static constexpr auto holderEncryptedInbox = EncryptedBalanceType::HolderEncryptedInbox;
+    static constexpr auto holderEncryptedSpending = EncryptedBalanceType::HolderEncryptedSpending;
+    static constexpr auto auditorEncryptedBalance = EncryptedBalanceType::AuditorEncryptedBalance;
+
     MPTTester(Env& env, Account issuer, MPTInit const& constr = {});
     MPTTester(MPTInitDef const& constr);
     MPTTester(
@@ -204,6 +442,83 @@ public:
     static json::Value
     setJV(MPTSet const& set = {});
 
+    void
+    convert(MPTConvert const& arg = MPTConvert{});
+
+    /**
+     * @brief Build a confidential convert JV without submitting it.
+     *
+     * @param arg Transaction builder arguments.
+     * @param seq Inner transaction sequence used in the Schnorr proof context
+     *            hash.
+     * @return The transaction JSON object.
+     */
+    json::Value
+    convertJV(MPTConvert const& arg, std::uint32_t seq);
+
+    void
+    mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
+
+    [[nodiscard]] json::Value
+    mergeInboxJV(MPTMergeInbox const& arg = MPTMergeInbox{}) const;
+
+    void
+    send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
+
+    /**
+     * @brief Build a confidential send JV.
+     *
+     * When chain is provided, the sender's proof parameters are taken from it
+     * instead of the ledger, enabling proof generation for a second or later
+     * send from the same account inside a single batch.
+     *
+     * @param arg Transaction builder arguments.
+     * @param seq Inner transaction sequence used in the proof context hash.
+     * @param chain Optional predicted sender state from a previous batched
+     *              send.
+     * @return The transaction JSON object.
+     */
+    json::Value
+    sendJV(
+        MPTConfidentialSend const& arg,
+        std::uint32_t seq,
+        std::optional chain = std::nullopt);
+
+    /**
+     * @brief Compute the projected sender state after a confidential send in a
+     * batch.
+     *
+     * Each confidential send requires a ZK proof that the sender's spending
+     * balance covers the transfer. In a batch, the second and later sends from
+     * the same sender need proofs built against the updated spending balance.
+     *
+     * @param sender The sender whose post-send state is being predicted.
+     * @param sendAmt The plaintext amount sent by the transaction.
+     * @param jv The confidential send transaction JSON object.
+     * @return The predicted sender state after applying the send.
+     */
+    [[nodiscard]] ConfidentialSendChainState
+    chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const;
+
+    void
+    convertBack(MPTConvertBack const& arg = MPTConvertBack{});
+
+    /**
+     * @brief Build a confidential convertBack JV without submitting it.
+     *
+     * Reads the current encrypted spending balance and version from the ledger,
+     * so call this before the batch is submitted.
+     *
+     * @param arg Transaction builder arguments.
+     * @param seq Inner transaction sequence used in the proof context hash.
+     * @return The transaction JSON object.
+     */
+    json::Value
+    convertBackJV(MPTConvertBack const& arg, std::uint32_t seq);
+
+    void
+    confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
+
     [[nodiscard]] bool
     checkDomainID(std::optional expected) const;
 
@@ -213,6 +528,9 @@ public:
     [[nodiscard]] bool
     checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const;
 
+    [[nodiscard]] bool
+    checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const;
+
     [[nodiscard]] bool
     checkFlags(uint32_t const expectedFlags, std::optional const& holder = std::nullopt)
         const;
@@ -265,6 +583,13 @@ public:
     [[nodiscard]] std::int64_t
     getBalance(Account const& account) const;
 
+    [[nodiscard]] std::int64_t
+    getIssuanceConfidentialBalance() const;
+
+    [[nodiscard]] std::optional
+    getEncryptedBalance(Account const& account, EncryptedBalanceType option = holderEncryptedInbox)
+        const;
+
     MPT
     operator[](std::string const& name) const;
 
@@ -273,6 +598,60 @@ public:
 
     operator Asset() const;
 
+    void
+    generateKeyPair(Account const& account);
+
+    [[nodiscard]] std::optional
+    getPubKey(Account const& account) const;
+
+    [[nodiscard]] std::optional
+    getPrivKey(Account const& account) const;
+
+    [[nodiscard]] Buffer
+    encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const;
+
+    [[nodiscard]] std::optional
+    decryptAmount(Account const& account, Buffer const& amt) const;
+
+    [[nodiscard]] std::optional
+    getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const;
+
+    [[nodiscard]] std::optional
+    getIssuanceOutstandingBalance() const;
+
+    [[nodiscard]] std::optional
+    getClawbackProof(
+        Account const& holder,
+        std::uint64_t amount,
+        Buffer const& privateKey,
+        uint256 const& txHash) const;
+
+    [[nodiscard]] std::optional
+    getSchnorrProof(Account const& account, uint256 const& ctxHash) const;
+
+    [[nodiscard]] std::optional
+    getConfidentialSendProof(
+        Account const& sender,
+        std::uint64_t const amount,
+        std::vector const& recipients,
+        Slice const& blindingFactor,
+        uint256 const& contextHash,
+        PedersenProofParams const& amountParams,
+        PedersenProofParams const& balanceParams) const;
+
+    [[nodiscard]] Buffer
+    getConvertBackProof(
+        Account const& holder,
+        std::uint64_t const amount,
+        uint256 const& contextHash,
+        PedersenProofParams const& pcParams) const;
+
+    [[nodiscard]] std::uint32_t
+    getMPTokenVersion(Account const account) const;
+
+    static Buffer
+    getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor);
+
     friend BookSpec
     operator~(MPTTester const& mpt)
     {
@@ -288,9 +667,54 @@ private:
 
     template 
     TER
-    submit(A const& arg, json::Value const& jv)
+    submit(A const& arg, json::Value jv)
     {
-        env_(jv, Txflags(arg.flags.value_or(0)), Ter(arg.err.value_or(tesSUCCESS)));
+        auto const expectedFlags = Txflags(arg.flags.value_or(0));
+        auto const expectedTer = Ter(arg.err.value_or(tesSUCCESS));
+
+        if constexpr (requires { arg.fee; })
+        {
+            if (arg.fee)
+                jv[jss::Fee] = to_string(*arg.fee);
+        }
+
+        std::optional ticketSeq;
+        if constexpr (requires { arg.ticketSeq; })
+            ticketSeq = arg.ticketSeq;
+
+        std::optional delegateAcct;
+        if constexpr (requires { arg.delegate; })
+            delegateAcct = arg.delegate;
+
+        std::optional dstTag;
+        if constexpr (requires { arg.destinationTag; })
+            dstTag = arg.destinationTag;
+
+        if (ticketSeq && delegateAcct)
+        {
+            env_(
+                jv,
+                expectedFlags,
+                expectedTer,
+                ticket::Use(*ticketSeq),
+                delegate::As(*delegateAcct));
+        }
+        else if (ticketSeq)
+        {
+            env_(jv, expectedFlags, expectedTer, ticket::Use(*ticketSeq));
+        }
+        else if (delegateAcct)
+        {
+            env_(jv, expectedFlags, expectedTer, delegate::As(*delegateAcct));
+        }
+        else if (dstTag)
+        {
+            env_(jv, expectedFlags, expectedTer, Dtag(*dstTag));
+        }
+        else
+        {
+            env_(jv, expectedFlags, expectedTer);
+        }
         auto const err = env_.ter();
         if (close_)
             env_.close();
@@ -309,6 +733,16 @@ private:
 
     [[nodiscard]] std::uint32_t
     getFlags(std::optional const& holder) const;
+
+    template 
+    void
+    fillConversionCiphertexts(
+        T const& arg,
+        json::Value& jv,
+        Buffer& holderCiphertext,
+        Buffer& issuerCiphertext,
+        std::optional& auditorCiphertext,
+        Buffer& blindingFactor) const;
 };
 
 }  // namespace xrpl::test::jtx
diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp
index 7479f0c63c..8dc5960ee0 100644
--- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp
@@ -34,6 +34,9 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
     auto const domainIDValue = canonical_UINT256();
     auto const mutableFlagsValue = canonical_UINT32();
     auto const referenceHoldingValue = canonical_UINT256();
+    auto const issuerEncryptionKeyValue = canonical_VL();
+    auto const auditorEncryptionKeyValue = canonical_VL();
+    auto const confidentialOutstandingAmountValue = canonical_UINT64();
 
     MPTokenIssuanceBuilder builder{
         issuerValue,
@@ -52,6 +55,9 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
     builder.setDomainID(domainIDValue);
     builder.setMutableFlags(mutableFlagsValue);
     builder.setReferenceHolding(referenceHoldingValue);
+    builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
+    builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
+    builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue);
 
     builder.setLedgerIndex(index);
     builder.setFlags(0x1u);
@@ -162,6 +168,30 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(entry.hasReferenceHolding());
     }
 
+    {
+        auto const& expected = issuerEncryptionKeyValue;
+        auto const actualOpt = entry.getIssuerEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey");
+        EXPECT_TRUE(entry.hasIssuerEncryptionKey());
+    }
+
+    {
+        auto const& expected = auditorEncryptionKeyValue;
+        auto const actualOpt = entry.getAuditorEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey");
+        EXPECT_TRUE(entry.hasAuditorEncryptionKey());
+    }
+
+    {
+        auto const& expected = confidentialOutstandingAmountValue;
+        auto const actualOpt = entry.getConfidentialOutstandingAmount();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfConfidentialOutstandingAmount");
+        EXPECT_TRUE(entry.hasConfidentialOutstandingAmount());
+    }
+
     EXPECT_TRUE(entry.hasLedgerIndex());
     auto const ledgerIndex = entry.getLedgerIndex();
     ASSERT_TRUE(ledgerIndex.has_value());
@@ -189,6 +219,9 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
     auto const domainIDValue = canonical_UINT256();
     auto const mutableFlagsValue = canonical_UINT32();
     auto const referenceHoldingValue = canonical_UINT256();
+    auto const issuerEncryptionKeyValue = canonical_VL();
+    auto const auditorEncryptionKeyValue = canonical_VL();
+    auto const confidentialOutstandingAmountValue = canonical_UINT64();
 
     auto sle = std::make_shared(MPTokenIssuance::entryType, index);
 
@@ -206,6 +239,9 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
     sle->at(sfDomainID) = domainIDValue;
     sle->at(sfMutableFlags) = mutableFlagsValue;
     sle->at(sfReferenceHolding) = referenceHoldingValue;
+    sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue;
+    sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
+    sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue;
 
     MPTokenIssuanceBuilder builderFromSle{sle};
     EXPECT_TRUE(builderFromSle.validate());
@@ -380,6 +416,45 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
         expectEqualField(expected, *fromBuilderOpt, "sfReferenceHolding");
     }
 
+    {
+        auto const& expected = issuerEncryptionKeyValue;
+
+        auto const fromSleOpt = entryFromSle.getIssuerEncryptionKey();
+        auto const fromBuilderOpt = entryFromBuilder.getIssuerEncryptionKey();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfIssuerEncryptionKey");
+        expectEqualField(expected, *fromBuilderOpt, "sfIssuerEncryptionKey");
+    }
+
+    {
+        auto const& expected = auditorEncryptionKeyValue;
+
+        auto const fromSleOpt = entryFromSle.getAuditorEncryptionKey();
+        auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptionKey();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptionKey");
+        expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey");
+    }
+
+    {
+        auto const& expected = confidentialOutstandingAmountValue;
+
+        auto const fromSleOpt = entryFromSle.getConfidentialOutstandingAmount();
+        auto const fromBuilderOpt = entryFromBuilder.getConfidentialOutstandingAmount();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfConfidentialOutstandingAmount");
+        expectEqualField(expected, *fromBuilderOpt, "sfConfidentialOutstandingAmount");
+    }
+
     EXPECT_EQ(entryFromSle.getKey(), index);
     EXPECT_EQ(entryFromBuilder.getKey(), index);
 }
@@ -460,5 +535,11 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(entry.getMutableFlags().has_value());
     EXPECT_FALSE(entry.hasReferenceHolding());
     EXPECT_FALSE(entry.getReferenceHolding().has_value());
+    EXPECT_FALSE(entry.hasIssuerEncryptionKey());
+    EXPECT_FALSE(entry.getIssuerEncryptionKey().has_value());
+    EXPECT_FALSE(entry.hasAuditorEncryptionKey());
+    EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value());
+    EXPECT_FALSE(entry.hasConfidentialOutstandingAmount());
+    EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value());
 }
 }
diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp
index c104e7b365..7db4b638a7 100644
--- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp
@@ -27,6 +27,12 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
     auto const ownerNodeValue = canonical_UINT64();
     auto const previousTxnIDValue = canonical_UINT256();
     auto const previousTxnLgrSeqValue = canonical_UINT32();
+    auto const confidentialBalanceInboxValue = canonical_VL();
+    auto const confidentialBalanceSpendingValue = canonical_VL();
+    auto const confidentialBalanceVersionValue = canonical_UINT32();
+    auto const issuerEncryptedBalanceValue = canonical_VL();
+    auto const auditorEncryptedBalanceValue = canonical_VL();
+    auto const holderEncryptionKeyValue = canonical_VL();
 
     MPTokenBuilder builder{
         accountValue,
@@ -38,6 +44,12 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
 
     builder.setMPTAmount(mPTAmountValue);
     builder.setLockedAmount(lockedAmountValue);
+    builder.setConfidentialBalanceInbox(confidentialBalanceInboxValue);
+    builder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue);
+    builder.setConfidentialBalanceVersion(confidentialBalanceVersionValue);
+    builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue);
+    builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue);
+    builder.setHolderEncryptionKey(holderEncryptionKeyValue);
 
     builder.setLedgerIndex(index);
     builder.setFlags(0x1u);
@@ -94,6 +106,54 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(entry.hasLockedAmount());
     }
 
+    {
+        auto const& expected = confidentialBalanceInboxValue;
+        auto const actualOpt = entry.getConfidentialBalanceInbox();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox");
+        EXPECT_TRUE(entry.hasConfidentialBalanceInbox());
+    }
+
+    {
+        auto const& expected = confidentialBalanceSpendingValue;
+        auto const actualOpt = entry.getConfidentialBalanceSpending();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending");
+        EXPECT_TRUE(entry.hasConfidentialBalanceSpending());
+    }
+
+    {
+        auto const& expected = confidentialBalanceVersionValue;
+        auto const actualOpt = entry.getConfidentialBalanceVersion();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfConfidentialBalanceVersion");
+        EXPECT_TRUE(entry.hasConfidentialBalanceVersion());
+    }
+
+    {
+        auto const& expected = issuerEncryptedBalanceValue;
+        auto const actualOpt = entry.getIssuerEncryptedBalance();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfIssuerEncryptedBalance");
+        EXPECT_TRUE(entry.hasIssuerEncryptedBalance());
+    }
+
+    {
+        auto const& expected = auditorEncryptedBalanceValue;
+        auto const actualOpt = entry.getAuditorEncryptedBalance();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedBalance");
+        EXPECT_TRUE(entry.hasAuditorEncryptedBalance());
+    }
+
+    {
+        auto const& expected = holderEncryptionKeyValue;
+        auto const actualOpt = entry.getHolderEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
+        EXPECT_TRUE(entry.hasHolderEncryptionKey());
+    }
+
     EXPECT_TRUE(entry.hasLedgerIndex());
     auto const ledgerIndex = entry.getLedgerIndex();
     ASSERT_TRUE(ledgerIndex.has_value());
@@ -114,6 +174,12 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
     auto const ownerNodeValue = canonical_UINT64();
     auto const previousTxnIDValue = canonical_UINT256();
     auto const previousTxnLgrSeqValue = canonical_UINT32();
+    auto const confidentialBalanceInboxValue = canonical_VL();
+    auto const confidentialBalanceSpendingValue = canonical_VL();
+    auto const confidentialBalanceVersionValue = canonical_UINT32();
+    auto const issuerEncryptedBalanceValue = canonical_VL();
+    auto const auditorEncryptedBalanceValue = canonical_VL();
+    auto const holderEncryptionKeyValue = canonical_VL();
 
     auto sle = std::make_shared(MPToken::entryType, index);
 
@@ -124,6 +190,12 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
     sle->at(sfOwnerNode) = ownerNodeValue;
     sle->at(sfPreviousTxnID) = previousTxnIDValue;
     sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
+    sle->at(sfConfidentialBalanceInbox) = confidentialBalanceInboxValue;
+    sle->at(sfConfidentialBalanceSpending) = confidentialBalanceSpendingValue;
+    sle->at(sfConfidentialBalanceVersion) = confidentialBalanceVersionValue;
+    sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue;
+    sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue;
+    sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
 
     MPTokenBuilder builderFromSle{sle};
     EXPECT_TRUE(builderFromSle.validate());
@@ -210,6 +282,84 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
         expectEqualField(expected, *fromBuilderOpt, "sfLockedAmount");
     }
 
+    {
+        auto const& expected = confidentialBalanceInboxValue;
+
+        auto const fromSleOpt = entryFromSle.getConfidentialBalanceInbox();
+        auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceInbox();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceInbox");
+        expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceInbox");
+    }
+
+    {
+        auto const& expected = confidentialBalanceSpendingValue;
+
+        auto const fromSleOpt = entryFromSle.getConfidentialBalanceSpending();
+        auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceSpending();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceSpending");
+        expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceSpending");
+    }
+
+    {
+        auto const& expected = confidentialBalanceVersionValue;
+
+        auto const fromSleOpt = entryFromSle.getConfidentialBalanceVersion();
+        auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceVersion();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceVersion");
+        expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceVersion");
+    }
+
+    {
+        auto const& expected = issuerEncryptedBalanceValue;
+
+        auto const fromSleOpt = entryFromSle.getIssuerEncryptedBalance();
+        auto const fromBuilderOpt = entryFromBuilder.getIssuerEncryptedBalance();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfIssuerEncryptedBalance");
+        expectEqualField(expected, *fromBuilderOpt, "sfIssuerEncryptedBalance");
+    }
+
+    {
+        auto const& expected = auditorEncryptedBalanceValue;
+
+        auto const fromSleOpt = entryFromSle.getAuditorEncryptedBalance();
+        auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptedBalance();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptedBalance");
+        expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedBalance");
+    }
+
+    {
+        auto const& expected = holderEncryptionKeyValue;
+
+        auto const fromSleOpt = entryFromSle.getHolderEncryptionKey();
+        auto const fromBuilderOpt = entryFromBuilder.getHolderEncryptionKey();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfHolderEncryptionKey");
+        expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey");
+    }
+
     EXPECT_EQ(entryFromSle.getKey(), index);
     EXPECT_EQ(entryFromBuilder.getKey(), index);
 }
@@ -276,5 +426,17 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(entry.getMPTAmount().has_value());
     EXPECT_FALSE(entry.hasLockedAmount());
     EXPECT_FALSE(entry.getLockedAmount().has_value());
+    EXPECT_FALSE(entry.hasConfidentialBalanceInbox());
+    EXPECT_FALSE(entry.getConfidentialBalanceInbox().has_value());
+    EXPECT_FALSE(entry.hasConfidentialBalanceSpending());
+    EXPECT_FALSE(entry.getConfidentialBalanceSpending().has_value());
+    EXPECT_FALSE(entry.hasConfidentialBalanceVersion());
+    EXPECT_FALSE(entry.getConfidentialBalanceVersion().has_value());
+    EXPECT_FALSE(entry.hasIssuerEncryptedBalance());
+    EXPECT_FALSE(entry.getIssuerEncryptedBalance().has_value());
+    EXPECT_FALSE(entry.hasAuditorEncryptedBalance());
+    EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value());
+    EXPECT_FALSE(entry.hasHolderEncryptionKey());
+    EXPECT_FALSE(entry.getHolderEncryptionKey().has_value());
 }
 }
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp
new file mode 100644
index 0000000000..611bc18465
--- /dev/null
+++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp
@@ -0,0 +1,194 @@
+// Auto-generated unit tests for transaction ConfidentialMPTClawback
+
+
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::transactions {
+
+// 1 & 4) Set fields via builder setters, build, then read them back via
+// wrapper getters. After build(), validate() should succeed.
+TEST(TransactionsConfidentialMPTClawbackTests, BuilderSettersRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTClawback"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 1;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const holderValue = canonical_ACCOUNT();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const zKProofValue = canonical_VL();
+
+    ConfidentialMPTClawbackBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        holderValue,
+        mPTAmountValue,
+        zKProofValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Set optional fields
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(tx.validate(reason)) << reason;
+
+    // Verify signing was applied
+    EXPECT_FALSE(tx.getSigningPubKey().empty());
+    EXPECT_TRUE(tx.hasTxnSignature());
+
+    // Verify common fields
+    EXPECT_EQ(tx.getAccount(), accountValue);
+    EXPECT_EQ(tx.getSequence(), sequenceValue);
+    EXPECT_EQ(tx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = tx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = holderValue;
+        auto const actual = tx.getHolder();
+        expectEqualField(expected, actual, "sfHolder");
+    }
+
+    {
+        auto const& expected = mPTAmountValue;
+        auto const actual = tx.getMPTAmount();
+        expectEqualField(expected, actual, "sfMPTAmount");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actual = tx.getZKProof();
+        expectEqualField(expected, actual, "sfZKProof");
+    }
+
+    // Verify optional fields
+}
+
+// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
+// and verify all fields match.
+TEST(TransactionsConfidentialMPTClawbackTests, BuilderFromStTxRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTClawbackFromTx"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 2;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const holderValue = canonical_ACCOUNT();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const zKProofValue = canonical_VL();
+
+    // Build an initial transaction
+    ConfidentialMPTClawbackBuilder initialBuilder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        holderValue,
+        mPTAmountValue,
+        zKProofValue,
+        sequenceValue,
+        feeValue
+    };
+
+
+    auto initialTx = initialBuilder.build(publicKey, secretKey);
+
+    // Create builder from existing STTx
+    ConfidentialMPTClawbackBuilder builderFromTx{initialTx.getSTTx()};
+
+    auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
+
+    // Verify common fields
+    EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
+    EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
+    EXPECT_EQ(rebuiltTx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = rebuiltTx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = holderValue;
+        auto const actual = rebuiltTx.getHolder();
+        expectEqualField(expected, actual, "sfHolder");
+    }
+
+    {
+        auto const& expected = mPTAmountValue;
+        auto const actual = rebuiltTx.getMPTAmount();
+        expectEqualField(expected, actual, "sfMPTAmount");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actual = rebuiltTx.getZKProof();
+        expectEqualField(expected, actual, "sfZKProof");
+    }
+
+    // Verify optional fields
+}
+
+// 3) Verify wrapper throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTClawbackTests, WrapperThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTClawback{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 4) Verify builder throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTClawbackTests, BuilderThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTClawbackBuilder{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+
+}
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp
new file mode 100644
index 0000000000..1a5fb3a046
--- /dev/null
+++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp
@@ -0,0 +1,303 @@
+// Auto-generated unit tests for transaction ConfidentialMPTConvertBack
+
+
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::transactions {
+
+// 1 & 4) Set fields via builder setters, build, then read them back via
+// wrapper getters. After build(), validate() should succeed.
+TEST(TransactionsConfidentialMPTConvertBackTests, BuilderSettersRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBack"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 1;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const holderEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const auditorEncryptedAmountValue = canonical_VL();
+    auto const blindingFactorValue = canonical_UINT256();
+    auto const zKProofValue = canonical_VL();
+    auto const balanceCommitmentValue = canonical_VL();
+
+    ConfidentialMPTConvertBackBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        mPTAmountValue,
+        holderEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        blindingFactorValue,
+        zKProofValue,
+        balanceCommitmentValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Set optional fields
+    builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(tx.validate(reason)) << reason;
+
+    // Verify signing was applied
+    EXPECT_FALSE(tx.getSigningPubKey().empty());
+    EXPECT_TRUE(tx.hasTxnSignature());
+
+    // Verify common fields
+    EXPECT_EQ(tx.getAccount(), accountValue);
+    EXPECT_EQ(tx.getSequence(), sequenceValue);
+    EXPECT_EQ(tx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = tx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = mPTAmountValue;
+        auto const actual = tx.getMPTAmount();
+        expectEqualField(expected, actual, "sfMPTAmount");
+    }
+
+    {
+        auto const& expected = holderEncryptedAmountValue;
+        auto const actual = tx.getHolderEncryptedAmount();
+        expectEqualField(expected, actual, "sfHolderEncryptedAmount");
+    }
+
+    {
+        auto const& expected = issuerEncryptedAmountValue;
+        auto const actual = tx.getIssuerEncryptedAmount();
+        expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
+    }
+
+    {
+        auto const& expected = blindingFactorValue;
+        auto const actual = tx.getBlindingFactor();
+        expectEqualField(expected, actual, "sfBlindingFactor");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actual = tx.getZKProof();
+        expectEqualField(expected, actual, "sfZKProof");
+    }
+
+    {
+        auto const& expected = balanceCommitmentValue;
+        auto const actual = tx.getBalanceCommitment();
+        expectEqualField(expected, actual, "sfBalanceCommitment");
+    }
+
+    // Verify optional fields
+    {
+        auto const& expected = auditorEncryptedAmountValue;
+        auto const actualOpt = tx.getAuditorEncryptedAmount();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
+        EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
+    }
+
+}
+
+// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
+// and verify all fields match.
+TEST(TransactionsConfidentialMPTConvertBackTests, BuilderFromStTxRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBackFromTx"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 2;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const holderEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const auditorEncryptedAmountValue = canonical_VL();
+    auto const blindingFactorValue = canonical_UINT256();
+    auto const zKProofValue = canonical_VL();
+    auto const balanceCommitmentValue = canonical_VL();
+
+    // Build an initial transaction
+    ConfidentialMPTConvertBackBuilder initialBuilder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        mPTAmountValue,
+        holderEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        blindingFactorValue,
+        zKProofValue,
+        balanceCommitmentValue,
+        sequenceValue,
+        feeValue
+    };
+
+    initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
+
+    auto initialTx = initialBuilder.build(publicKey, secretKey);
+
+    // Create builder from existing STTx
+    ConfidentialMPTConvertBackBuilder builderFromTx{initialTx.getSTTx()};
+
+    auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
+
+    // Verify common fields
+    EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
+    EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
+    EXPECT_EQ(rebuiltTx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = rebuiltTx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = mPTAmountValue;
+        auto const actual = rebuiltTx.getMPTAmount();
+        expectEqualField(expected, actual, "sfMPTAmount");
+    }
+
+    {
+        auto const& expected = holderEncryptedAmountValue;
+        auto const actual = rebuiltTx.getHolderEncryptedAmount();
+        expectEqualField(expected, actual, "sfHolderEncryptedAmount");
+    }
+
+    {
+        auto const& expected = issuerEncryptedAmountValue;
+        auto const actual = rebuiltTx.getIssuerEncryptedAmount();
+        expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
+    }
+
+    {
+        auto const& expected = blindingFactorValue;
+        auto const actual = rebuiltTx.getBlindingFactor();
+        expectEqualField(expected, actual, "sfBlindingFactor");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actual = rebuiltTx.getZKProof();
+        expectEqualField(expected, actual, "sfZKProof");
+    }
+
+    {
+        auto const& expected = balanceCommitmentValue;
+        auto const actual = rebuiltTx.getBalanceCommitment();
+        expectEqualField(expected, actual, "sfBalanceCommitment");
+    }
+
+    // Verify optional fields
+    {
+        auto const& expected = auditorEncryptedAmountValue;
+        auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
+    }
+
+}
+
+// 3) Verify wrapper throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTConvertBackTests, WrapperThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTConvertBack{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 4) Verify builder throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTConvertBackTests, BuilderThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTConvertBackBuilder{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 5) Build with only required fields and verify optional fields return nullopt.
+TEST(TransactionsConfidentialMPTConvertBackTests, OptionalFieldsReturnNullopt)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBackNullopt"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 3;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific required field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const holderEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const blindingFactorValue = canonical_UINT256();
+    auto const zKProofValue = canonical_VL();
+    auto const balanceCommitmentValue = canonical_VL();
+
+    ConfidentialMPTConvertBackBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        mPTAmountValue,
+        holderEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        blindingFactorValue,
+        zKProofValue,
+        balanceCommitmentValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Do NOT set optional fields
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    // Verify optional fields are not present
+    EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
+    EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
+}
+
+}
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp
new file mode 100644
index 0000000000..6110196353
--- /dev/null
+++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp
@@ -0,0 +1,309 @@
+// Auto-generated unit tests for transaction ConfidentialMPTConvert
+
+
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::transactions {
+
+// 1 & 4) Set fields via builder setters, build, then read them back via
+// wrapper getters. After build(), validate() should succeed.
+TEST(TransactionsConfidentialMPTConvertTests, BuilderSettersRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvert"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 1;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const holderEncryptionKeyValue = canonical_VL();
+    auto const holderEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const auditorEncryptedAmountValue = canonical_VL();
+    auto const blindingFactorValue = canonical_UINT256();
+    auto const zKProofValue = canonical_VL();
+
+    ConfidentialMPTConvertBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        mPTAmountValue,
+        holderEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        blindingFactorValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Set optional fields
+    builder.setHolderEncryptionKey(holderEncryptionKeyValue);
+    builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
+    builder.setZKProof(zKProofValue);
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(tx.validate(reason)) << reason;
+
+    // Verify signing was applied
+    EXPECT_FALSE(tx.getSigningPubKey().empty());
+    EXPECT_TRUE(tx.hasTxnSignature());
+
+    // Verify common fields
+    EXPECT_EQ(tx.getAccount(), accountValue);
+    EXPECT_EQ(tx.getSequence(), sequenceValue);
+    EXPECT_EQ(tx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = tx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = mPTAmountValue;
+        auto const actual = tx.getMPTAmount();
+        expectEqualField(expected, actual, "sfMPTAmount");
+    }
+
+    {
+        auto const& expected = holderEncryptedAmountValue;
+        auto const actual = tx.getHolderEncryptedAmount();
+        expectEqualField(expected, actual, "sfHolderEncryptedAmount");
+    }
+
+    {
+        auto const& expected = issuerEncryptedAmountValue;
+        auto const actual = tx.getIssuerEncryptedAmount();
+        expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
+    }
+
+    {
+        auto const& expected = blindingFactorValue;
+        auto const actual = tx.getBlindingFactor();
+        expectEqualField(expected, actual, "sfBlindingFactor");
+    }
+
+    // Verify optional fields
+    {
+        auto const& expected = holderEncryptionKeyValue;
+        auto const actualOpt = tx.getHolderEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present";
+        expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
+        EXPECT_TRUE(tx.hasHolderEncryptionKey());
+    }
+
+    {
+        auto const& expected = auditorEncryptedAmountValue;
+        auto const actualOpt = tx.getAuditorEncryptedAmount();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
+        EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actualOpt = tx.getZKProof();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present";
+        expectEqualField(expected, *actualOpt, "sfZKProof");
+        EXPECT_TRUE(tx.hasZKProof());
+    }
+
+}
+
+// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
+// and verify all fields match.
+TEST(TransactionsConfidentialMPTConvertTests, BuilderFromStTxRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertFromTx"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 2;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const holderEncryptionKeyValue = canonical_VL();
+    auto const holderEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const auditorEncryptedAmountValue = canonical_VL();
+    auto const blindingFactorValue = canonical_UINT256();
+    auto const zKProofValue = canonical_VL();
+
+    // Build an initial transaction
+    ConfidentialMPTConvertBuilder initialBuilder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        mPTAmountValue,
+        holderEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        blindingFactorValue,
+        sequenceValue,
+        feeValue
+    };
+
+    initialBuilder.setHolderEncryptionKey(holderEncryptionKeyValue);
+    initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
+    initialBuilder.setZKProof(zKProofValue);
+
+    auto initialTx = initialBuilder.build(publicKey, secretKey);
+
+    // Create builder from existing STTx
+    ConfidentialMPTConvertBuilder builderFromTx{initialTx.getSTTx()};
+
+    auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
+
+    // Verify common fields
+    EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
+    EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
+    EXPECT_EQ(rebuiltTx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = rebuiltTx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = mPTAmountValue;
+        auto const actual = rebuiltTx.getMPTAmount();
+        expectEqualField(expected, actual, "sfMPTAmount");
+    }
+
+    {
+        auto const& expected = holderEncryptedAmountValue;
+        auto const actual = rebuiltTx.getHolderEncryptedAmount();
+        expectEqualField(expected, actual, "sfHolderEncryptedAmount");
+    }
+
+    {
+        auto const& expected = issuerEncryptedAmountValue;
+        auto const actual = rebuiltTx.getIssuerEncryptedAmount();
+        expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
+    }
+
+    {
+        auto const& expected = blindingFactorValue;
+        auto const actual = rebuiltTx.getBlindingFactor();
+        expectEqualField(expected, actual, "sfBlindingFactor");
+    }
+
+    // Verify optional fields
+    {
+        auto const& expected = holderEncryptionKeyValue;
+        auto const actualOpt = rebuiltTx.getHolderEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present";
+        expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey");
+    }
+
+    {
+        auto const& expected = auditorEncryptedAmountValue;
+        auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actualOpt = rebuiltTx.getZKProof();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present";
+        expectEqualField(expected, *actualOpt, "sfZKProof");
+    }
+
+}
+
+// 3) Verify wrapper throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTConvertTests, WrapperThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTConvert{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 4) Verify builder throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTConvertTests, BuilderThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTConvertBuilder{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 5) Build with only required fields and verify optional fields return nullopt.
+TEST(TransactionsConfidentialMPTConvertTests, OptionalFieldsReturnNullopt)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertNullopt"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 3;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific required field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const mPTAmountValue = canonical_UINT64();
+    auto const holderEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const blindingFactorValue = canonical_UINT256();
+
+    ConfidentialMPTConvertBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        mPTAmountValue,
+        holderEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        blindingFactorValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Do NOT set optional fields
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    // Verify optional fields are not present
+    EXPECT_FALSE(tx.hasHolderEncryptionKey());
+    EXPECT_FALSE(tx.getHolderEncryptionKey().has_value());
+    EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
+    EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
+    EXPECT_FALSE(tx.hasZKProof());
+    EXPECT_FALSE(tx.getZKProof().has_value());
+}
+
+}
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp
new file mode 100644
index 0000000000..bfc3ec4f0d
--- /dev/null
+++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp
@@ -0,0 +1,146 @@
+// Auto-generated unit tests for transaction ConfidentialMPTMergeInbox
+
+
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::transactions {
+
+// 1 & 4) Set fields via builder setters, build, then read them back via
+// wrapper getters. After build(), validate() should succeed.
+TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderSettersRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMergeInbox"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 1;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+
+    ConfidentialMPTMergeInboxBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Set optional fields
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(tx.validate(reason)) << reason;
+
+    // Verify signing was applied
+    EXPECT_FALSE(tx.getSigningPubKey().empty());
+    EXPECT_TRUE(tx.hasTxnSignature());
+
+    // Verify common fields
+    EXPECT_EQ(tx.getAccount(), accountValue);
+    EXPECT_EQ(tx.getSequence(), sequenceValue);
+    EXPECT_EQ(tx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = tx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    // Verify optional fields
+}
+
+// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
+// and verify all fields match.
+TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderFromStTxRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMergeInboxFromTx"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 2;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+
+    // Build an initial transaction
+    ConfidentialMPTMergeInboxBuilder initialBuilder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        sequenceValue,
+        feeValue
+    };
+
+
+    auto initialTx = initialBuilder.build(publicKey, secretKey);
+
+    // Create builder from existing STTx
+    ConfidentialMPTMergeInboxBuilder builderFromTx{initialTx.getSTTx()};
+
+    auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
+
+    // Verify common fields
+    EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
+    EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
+    EXPECT_EQ(rebuiltTx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = rebuiltTx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    // Verify optional fields
+}
+
+// 3) Verify wrapper throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTMergeInboxTests, WrapperThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTMergeInbox{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 4) Verify builder throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTMergeInboxBuilder{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+
+}
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp
new file mode 100644
index 0000000000..ca7793dece
--- /dev/null
+++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp
@@ -0,0 +1,363 @@
+// Auto-generated unit tests for transaction ConfidentialMPTSend
+
+
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::transactions {
+
+// 1 & 4) Set fields via builder setters, build, then read them back via
+// wrapper getters. After build(), validate() should succeed.
+TEST(TransactionsConfidentialMPTSendTests, BuilderSettersRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSend"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 1;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const destinationValue = canonical_ACCOUNT();
+    auto const destinationTagValue = canonical_UINT32();
+    auto const senderEncryptedAmountValue = canonical_VL();
+    auto const destinationEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const auditorEncryptedAmountValue = canonical_VL();
+    auto const zKProofValue = canonical_VL();
+    auto const amountCommitmentValue = canonical_VL();
+    auto const balanceCommitmentValue = canonical_VL();
+    auto const credentialIDsValue = canonical_VECTOR256();
+
+    ConfidentialMPTSendBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        destinationValue,
+        senderEncryptedAmountValue,
+        destinationEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        zKProofValue,
+        amountCommitmentValue,
+        balanceCommitmentValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Set optional fields
+    builder.setDestinationTag(destinationTagValue);
+    builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
+    builder.setCredentialIDs(credentialIDsValue);
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(tx.validate(reason)) << reason;
+
+    // Verify signing was applied
+    EXPECT_FALSE(tx.getSigningPubKey().empty());
+    EXPECT_TRUE(tx.hasTxnSignature());
+
+    // Verify common fields
+    EXPECT_EQ(tx.getAccount(), accountValue);
+    EXPECT_EQ(tx.getSequence(), sequenceValue);
+    EXPECT_EQ(tx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = tx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = destinationValue;
+        auto const actual = tx.getDestination();
+        expectEqualField(expected, actual, "sfDestination");
+    }
+
+    {
+        auto const& expected = senderEncryptedAmountValue;
+        auto const actual = tx.getSenderEncryptedAmount();
+        expectEqualField(expected, actual, "sfSenderEncryptedAmount");
+    }
+
+    {
+        auto const& expected = destinationEncryptedAmountValue;
+        auto const actual = tx.getDestinationEncryptedAmount();
+        expectEqualField(expected, actual, "sfDestinationEncryptedAmount");
+    }
+
+    {
+        auto const& expected = issuerEncryptedAmountValue;
+        auto const actual = tx.getIssuerEncryptedAmount();
+        expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actual = tx.getZKProof();
+        expectEqualField(expected, actual, "sfZKProof");
+    }
+
+    {
+        auto const& expected = amountCommitmentValue;
+        auto const actual = tx.getAmountCommitment();
+        expectEqualField(expected, actual, "sfAmountCommitment");
+    }
+
+    {
+        auto const& expected = balanceCommitmentValue;
+        auto const actual = tx.getBalanceCommitment();
+        expectEqualField(expected, actual, "sfBalanceCommitment");
+    }
+
+    // Verify optional fields
+    {
+        auto const& expected = destinationTagValue;
+        auto const actualOpt = tx.getDestinationTag();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
+        expectEqualField(expected, *actualOpt, "sfDestinationTag");
+        EXPECT_TRUE(tx.hasDestinationTag());
+    }
+
+    {
+        auto const& expected = auditorEncryptedAmountValue;
+        auto const actualOpt = tx.getAuditorEncryptedAmount();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
+        EXPECT_TRUE(tx.hasAuditorEncryptedAmount());
+    }
+
+    {
+        auto const& expected = credentialIDsValue;
+        auto const actualOpt = tx.getCredentialIDs();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
+        expectEqualField(expected, *actualOpt, "sfCredentialIDs");
+        EXPECT_TRUE(tx.hasCredentialIDs());
+    }
+
+}
+
+// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
+// and verify all fields match.
+TEST(TransactionsConfidentialMPTSendTests, BuilderFromStTxRoundTrip)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSendFromTx"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 2;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const destinationValue = canonical_ACCOUNT();
+    auto const destinationTagValue = canonical_UINT32();
+    auto const senderEncryptedAmountValue = canonical_VL();
+    auto const destinationEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const auditorEncryptedAmountValue = canonical_VL();
+    auto const zKProofValue = canonical_VL();
+    auto const amountCommitmentValue = canonical_VL();
+    auto const balanceCommitmentValue = canonical_VL();
+    auto const credentialIDsValue = canonical_VECTOR256();
+
+    // Build an initial transaction
+    ConfidentialMPTSendBuilder initialBuilder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        destinationValue,
+        senderEncryptedAmountValue,
+        destinationEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        zKProofValue,
+        amountCommitmentValue,
+        balanceCommitmentValue,
+        sequenceValue,
+        feeValue
+    };
+
+    initialBuilder.setDestinationTag(destinationTagValue);
+    initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
+    initialBuilder.setCredentialIDs(credentialIDsValue);
+
+    auto initialTx = initialBuilder.build(publicKey, secretKey);
+
+    // Create builder from existing STTx
+    ConfidentialMPTSendBuilder builderFromTx{initialTx.getSTTx()};
+
+    auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
+
+    std::string reason;
+    EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
+
+    // Verify common fields
+    EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
+    EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
+    EXPECT_EQ(rebuiltTx.getFee(), feeValue);
+
+    // Verify required fields
+    {
+        auto const& expected = mPTokenIssuanceIDValue;
+        auto const actual = rebuiltTx.getMPTokenIssuanceID();
+        expectEqualField(expected, actual, "sfMPTokenIssuanceID");
+    }
+
+    {
+        auto const& expected = destinationValue;
+        auto const actual = rebuiltTx.getDestination();
+        expectEqualField(expected, actual, "sfDestination");
+    }
+
+    {
+        auto const& expected = senderEncryptedAmountValue;
+        auto const actual = rebuiltTx.getSenderEncryptedAmount();
+        expectEqualField(expected, actual, "sfSenderEncryptedAmount");
+    }
+
+    {
+        auto const& expected = destinationEncryptedAmountValue;
+        auto const actual = rebuiltTx.getDestinationEncryptedAmount();
+        expectEqualField(expected, actual, "sfDestinationEncryptedAmount");
+    }
+
+    {
+        auto const& expected = issuerEncryptedAmountValue;
+        auto const actual = rebuiltTx.getIssuerEncryptedAmount();
+        expectEqualField(expected, actual, "sfIssuerEncryptedAmount");
+    }
+
+    {
+        auto const& expected = zKProofValue;
+        auto const actual = rebuiltTx.getZKProof();
+        expectEqualField(expected, actual, "sfZKProof");
+    }
+
+    {
+        auto const& expected = amountCommitmentValue;
+        auto const actual = rebuiltTx.getAmountCommitment();
+        expectEqualField(expected, actual, "sfAmountCommitment");
+    }
+
+    {
+        auto const& expected = balanceCommitmentValue;
+        auto const actual = rebuiltTx.getBalanceCommitment();
+        expectEqualField(expected, actual, "sfBalanceCommitment");
+    }
+
+    // Verify optional fields
+    {
+        auto const& expected = destinationTagValue;
+        auto const actualOpt = rebuiltTx.getDestinationTag();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
+        expectEqualField(expected, *actualOpt, "sfDestinationTag");
+    }
+
+    {
+        auto const& expected = auditorEncryptedAmountValue;
+        auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount");
+    }
+
+    {
+        auto const& expected = credentialIDsValue;
+        auto const actualOpt = rebuiltTx.getCredentialIDs();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
+        expectEqualField(expected, *actualOpt, "sfCredentialIDs");
+    }
+
+}
+
+// 3) Verify wrapper throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTSendTests, WrapperThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTSend{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 4) Verify builder throws when constructed from wrong transaction type.
+TEST(TransactionsConfidentialMPTSendTests, BuilderThrowsOnWrongTxType)
+{
+    // Build a valid transaction of a different type
+    auto const [pk, sk] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
+    auto const account = calcAccountID(pk);
+
+    AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
+    auto wrongTx = wrongBuilder.build(pk, sk);
+
+    EXPECT_THROW(ConfidentialMPTSendBuilder{wrongTx.getSTTx()}, std::runtime_error);
+}
+
+// 5) Build with only required fields and verify optional fields return nullopt.
+TEST(TransactionsConfidentialMPTSendTests, OptionalFieldsReturnNullopt)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSendNullopt"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 3;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific required field values
+    auto const mPTokenIssuanceIDValue = canonical_UINT192();
+    auto const destinationValue = canonical_ACCOUNT();
+    auto const senderEncryptedAmountValue = canonical_VL();
+    auto const destinationEncryptedAmountValue = canonical_VL();
+    auto const issuerEncryptedAmountValue = canonical_VL();
+    auto const zKProofValue = canonical_VL();
+    auto const amountCommitmentValue = canonical_VL();
+    auto const balanceCommitmentValue = canonical_VL();
+
+    ConfidentialMPTSendBuilder builder{
+        accountValue,
+        mPTokenIssuanceIDValue,
+        destinationValue,
+        senderEncryptedAmountValue,
+        destinationEncryptedAmountValue,
+        issuerEncryptedAmountValue,
+        zKProofValue,
+        amountCommitmentValue,
+        balanceCommitmentValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Do NOT set optional fields
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    // Verify optional fields are not present
+    EXPECT_FALSE(tx.hasDestinationTag());
+    EXPECT_FALSE(tx.getDestinationTag().has_value());
+    EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
+    EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
+    EXPECT_FALSE(tx.hasCredentialIDs());
+    EXPECT_FALSE(tx.getCredentialIDs().has_value());
+}
+
+}
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp
index 5c97ae8871..e7b34590b2 100644
--- a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp
@@ -35,6 +35,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
     auto const mPTokenMetadataValue = canonical_VL();
     auto const transferFeeValue = canonical_UINT16();
     auto const mutableFlagsValue = canonical_UINT32();
+    auto const issuerEncryptionKeyValue = canonical_VL();
+    auto const auditorEncryptionKeyValue = canonical_VL();
 
     MPTokenIssuanceSetBuilder builder{
         accountValue,
@@ -49,6 +51,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
     builder.setMPTokenMetadata(mPTokenMetadataValue);
     builder.setTransferFee(transferFeeValue);
     builder.setMutableFlags(mutableFlagsValue);
+    builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
+    builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
 
     auto tx = builder.build(publicKey, secretKey);
 
@@ -112,6 +116,22 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(tx.hasMutableFlags());
     }
 
+    {
+        auto const& expected = issuerEncryptionKeyValue;
+        auto const actualOpt = tx.getIssuerEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptionKey should be present";
+        expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey");
+        EXPECT_TRUE(tx.hasIssuerEncryptionKey());
+    }
+
+    {
+        auto const& expected = auditorEncryptionKeyValue;
+        auto const actualOpt = tx.getAuditorEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey");
+        EXPECT_TRUE(tx.hasAuditorEncryptionKey());
+    }
+
 }
 
 // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -134,6 +154,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
     auto const mPTokenMetadataValue = canonical_VL();
     auto const transferFeeValue = canonical_UINT16();
     auto const mutableFlagsValue = canonical_UINT32();
+    auto const issuerEncryptionKeyValue = canonical_VL();
+    auto const auditorEncryptionKeyValue = canonical_VL();
 
     // Build an initial transaction
     MPTokenIssuanceSetBuilder initialBuilder{
@@ -148,6 +170,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
     initialBuilder.setMPTokenMetadata(mPTokenMetadataValue);
     initialBuilder.setTransferFee(transferFeeValue);
     initialBuilder.setMutableFlags(mutableFlagsValue);
+    initialBuilder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
+    initialBuilder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
 
     auto initialTx = initialBuilder.build(publicKey, secretKey);
 
@@ -207,6 +231,20 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
         expectEqualField(expected, *actualOpt, "sfMutableFlags");
     }
 
+    {
+        auto const& expected = issuerEncryptionKeyValue;
+        auto const actualOpt = rebuiltTx.getIssuerEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptionKey should be present";
+        expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey");
+    }
+
+    {
+        auto const& expected = auditorEncryptionKeyValue;
+        auto const actualOpt = rebuiltTx.getAuditorEncryptionKey();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present";
+        expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey");
+    }
+
 }
 
 // 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -274,6 +312,10 @@ TEST(TransactionsMPTokenIssuanceSetTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(tx.getTransferFee().has_value());
     EXPECT_FALSE(tx.hasMutableFlags());
     EXPECT_FALSE(tx.getMutableFlags().has_value());
+    EXPECT_FALSE(tx.hasIssuerEncryptionKey());
+    EXPECT_FALSE(tx.getIssuerEncryptionKey().has_value());
+    EXPECT_FALSE(tx.hasAuditorEncryptionKey());
+    EXPECT_FALSE(tx.getAuditorEncryptionKey().has_value());
 }
 
 }

From 74b55a59b2c643929cba2352eef01c25be5ed479 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Mon, 29 Jun 2026 14:20:17 +0100
Subject: [PATCH 022/100] chore: Enable most bugprone checks (#7643)

---
 .clang-tidy                                   | 18 -----
 include/xrpl/config/BasicConfig.h             |  3 +-
 include/xrpl/protocol/detail/token_errors.h   |  1 -
 src/libxrpl/conditions/Condition.cpp          |  9 ---
 src/libxrpl/conditions/Fulfillment.cpp        | 12 ---
 src/libxrpl/ledger/helpers/TokenHelpers.cpp   |  7 +-
 .../nodestore/backend/RocksDBFactory.cpp      |  8 +-
 .../tx/transactors/dex/AMMWithdraw.cpp        |  7 +-
 src/test/app/AccountSet_test.cpp              |  1 +
 src/test/app/LoanBroker_test.cpp              |  9 ++-
 src/test/app/MPToken_test.cpp                 | 15 +---
 src/test/app/NFTokenBurn_test.cpp             |  1 +
 src/test/app/Vault_test.cpp                   |  3 +
 src/test/app/XChain_test.cpp                  | 10 +--
 src/test/consensus/LedgerTrie_test.cpp        |  1 +
 src/test/csf/Sim.h                            |  1 +
 src/test/nodestore/TestBase.h                 |  2 -
 src/test/rpc/LedgerEntry_test.cpp             |  2 -
 src/test/unit_test/SuiteJournal.h             |  3 +-
 src/tests/libxrpl/tx/AccountSet.cpp           |  1 +
 src/xrpld/core/detail/Config.cpp              | 10 +--
 src/xrpld/rpc/detail/PathRequestManager.cpp   |  3 +-
 src/xrpld/rpc/detail/Pathfinder.cpp           | 77 +++++++------------
 23 files changed, 61 insertions(+), 143 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index ef55e8517c..e12c73cc56 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -1,29 +1,11 @@
 ---
 Checks: "-*,
   bugprone-*,
-  -bugprone-assignment-in-if-condition,
-  -bugprone-bitwise-pointer-cast,
-  -bugprone-branch-clone,
-  -bugprone-command-processor,
-  -bugprone-copy-constructor-mutates-argument,
-  -bugprone-default-operator-new-on-overaligned-type,
   -bugprone-easily-swappable-parameters,
-  -bugprone-exception-copy-constructor-throws,
   -bugprone-exception-escape,
-  -bugprone-float-loop-counter,
-  -bugprone-forwarding-reference-overload,
   -bugprone-implicit-widening-of-multiplication-result,
-  -bugprone-incorrect-enable-shared-from-this,
   -bugprone-narrowing-conversions,
-  -bugprone-nondeterministic-pointer-iteration-order,
-  -bugprone-not-null-terminated-result,
-  -bugprone-random-generator-seed,
-  -bugprone-raw-memory-call-on-non-trivial-type,
-  -bugprone-std-namespace-modification,
-  -bugprone-tagged-union-member-count,
   -bugprone-throwing-static-initialization,
-  -bugprone-unchecked-string-to-number-conversion,
-  -bugprone-unintended-char-ostream-output,
 
   cppcoreguidelines-*,
   -cppcoreguidelines-avoid-c-arrays,
diff --git a/include/xrpl/config/BasicConfig.h b/include/xrpl/config/BasicConfig.h
index 5a82f7d081..858bf8bf2e 100644
--- a/include/xrpl/config/BasicConfig.h
+++ b/include/xrpl/config/BasicConfig.h
@@ -298,7 +298,8 @@ set(T& target, std::string const& name, Section const& section)
     try
     {
         auto const val = section.get(name);
-        if ((foundAndValid = val.has_value()))
+        foundAndValid = val.has_value();
+        if (foundAndValid)
             target = *val;
     }
     catch (boost::bad_lexical_cast const&)  // NOLINT(bugprone-empty-catch)
diff --git a/include/xrpl/protocol/detail/token_errors.h b/include/xrpl/protocol/detail/token_errors.h
index 9d5e98e646..97db9288f9 100644
--- a/include/xrpl/protocol/detail/token_errors.h
+++ b/include/xrpl/protocol/detail/token_errors.h
@@ -58,7 +58,6 @@ public:
             case TokenCodecErrc::InvalidEncodingChar:
                 return "invalid encoding char";
             case TokenCodecErrc::Unknown:
-                return "unknown";
             default:
                 return "unknown";
         }
diff --git a/src/libxrpl/conditions/Condition.cpp b/src/libxrpl/conditions/Condition.cpp
index cb950c9e24..3be7aa7757 100644
--- a/src/libxrpl/conditions/Condition.cpp
+++ b/src/libxrpl/conditions/Condition.cpp
@@ -190,17 +190,8 @@ Condition::deserialize(Slice s, std::error_code& ec)
             break;
 
         case 1:  // PrefixSha256
-            ec = Error::UnsupportedType;
-            return {};
-
         case 2:  // ThresholdSha256
-            ec = Error::UnsupportedType;
-            return {};
-
         case 3:  // RsaSha256
-            ec = Error::UnsupportedType;
-            return {};
-
         case 4:  // Ed25519Sha256
             ec = Error::UnsupportedType;
             return {};
diff --git a/src/libxrpl/conditions/Fulfillment.cpp b/src/libxrpl/conditions/Fulfillment.cpp
index eaad6b8cdb..23a2a5eb32 100644
--- a/src/libxrpl/conditions/Fulfillment.cpp
+++ b/src/libxrpl/conditions/Fulfillment.cpp
@@ -101,20 +101,8 @@ Fulfillment::deserialize(Slice s, std::error_code& ec)
             break;
 
         case safeCast(Type::PrefixSha256):
-            ec = Error::UnsupportedType;
-            return {};
-            break;
-
         case safeCast(Type::ThresholdSha256):
-            ec = Error::UnsupportedType;
-            return {};
-            break;
-
         case safeCast(Type::RsaSha256):
-            ec = Error::UnsupportedType;
-            return {};
-            break;
-
         case safeCast(Type::Ed25519Sha256):
             ec = Error::UnsupportedType;
             return {};
diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
index 2ecde25754..7988e24a56 100644
--- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
@@ -411,11 +411,8 @@ accountHolds(
 
     auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account));
 
-    if (!sleMpt)
-    {
-        amount.clear(mptIssue);
-    }
-    else if (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue))
+    if (!sleMpt ||
+        (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue)))
     {
         amount.clear(mptIssue);
     }
diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
index 565fbded5d..69a648f2f4 100644
--- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
+++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
@@ -36,7 +36,6 @@
 #include 
 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -286,7 +285,7 @@ public:
         Status status = Status::Ok;
 
         rocksdb::ReadOptions const options;
-        rocksdb::Slice const slice(std::bit_cast(hash.data()), keyBytes);
+        rocksdb::Slice const slice(reinterpret_cast(hash.data()), keyBytes);
 
         std::string string;
 
@@ -349,8 +348,9 @@ public:
             EncodedBlob const encoded(e);
 
             wb.Put(
-                rocksdb::Slice(std::bit_cast(encoded.getKey()), keyBytes),
-                rocksdb::Slice(std::bit_cast(encoded.getData()), encoded.getSize()));
+                rocksdb::Slice(reinterpret_cast(encoded.getKey()), keyBytes),
+                rocksdb::Slice(
+                    reinterpret_cast(encoded.getData()), encoded.getSize()));
         }
 
         rocksdb::WriteOptions const options;
diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
index f26164c5fe..14ed5a4646 100644
--- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
@@ -90,12 +90,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx)
         if (lpTokens || amount || amount2 || ePrice)
             return temMALFORMED;
     }
-    else if (ctx.tx.isFlag(tfOneAssetWithdrawAll))
-    {
-        if (!amount || lpTokens || amount2 || ePrice)
-            return temMALFORMED;
-    }
-    else if (ctx.tx.isFlag(tfSingleAsset))
+    else if (ctx.tx.isFlag(tfOneAssetWithdrawAll) || ctx.tx.isFlag(tfSingleAsset))
     {
         if (!amount || lpTokens || amount2 || ePrice)
             return temMALFORMED;
diff --git a/src/test/app/AccountSet_test.cpp b/src/test/app/AccountSet_test.cpp
index 6688e3693f..362113aec2 100644
--- a/src/test/app/AccountSet_test.cpp
+++ b/src/test/app/AccountSet_test.cpp
@@ -383,6 +383,7 @@ public:
         auto const usd = gw["USD"];
 
         // Test gateway with a variety of allowed transfer rates
+        // NOLINTNEXTLINE(bugprone-float-loop-counter)
         for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125)
         {
             Env env(*this);
diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp
index bdc950eeff..2a4de18a9c 100644
--- a/src/test/app/LoanBroker_test.cpp
+++ b/src/test/app/LoanBroker_test.cpp
@@ -283,7 +283,8 @@ class LoanBroker_test : public beast::unit_test::Suite
                 [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) {
                     using namespace jtx;
 
-                    if (BEAST_EXPECT(broker = env.le(keylet)))
+                    broker = env.le(keylet);
+                    if (BEAST_EXPECT(broker))
                     {
                         auto const amount = vault.asset(n);
                         BEAST_EXPECT(broker->at(sfCoverAvailable) == amount.number());
@@ -473,7 +474,8 @@ class LoanBroker_test : public beast::unit_test::Suite
             env.close();
 
             // Check the results of modifications
-            if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker)
+            broker = env.le(keylet);
+            if (BEAST_EXPECT(broker) && checkChangedBroker)
                 checkChangedBroker(broker);
 
             // Verify that fields get removed when set to default values
@@ -486,7 +488,8 @@ class LoanBroker_test : public beast::unit_test::Suite
             env.close();
 
             // Check the updated fields
-            if (BEAST_EXPECT(broker = env.le(keylet)))
+            broker = env.le(keylet);
+            if (BEAST_EXPECT(broker))
             {
                 BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
                 BEAST_EXPECT(!broker->isFieldPresent(sfData));
diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp
index 15e17b4536..bbf1c54ab1 100644
--- a/src/test/app/MPToken_test.cpp
+++ b/src/test/app/MPToken_test.cpp
@@ -111,20 +111,9 @@ class MPToken_test : public beast::unit_test::Suite
                  .metadata = "test",
                  .err = temMALFORMED});
 
-            if (!features[featureSingleAssetVault])
+            if (!features[featureSingleAssetVault] || !features[featurePermissionedDomains])
             {
-                // tries to set DomainID when SAV is disabled
-                mptAlice.create(
-                    {.maxAmt = 100,
-                     .assetScale = 0,
-                     .metadata = "test",
-                     .flags = tfMPTRequireAuth,
-                     .domainID = uint256(42),
-                     .err = temDISABLED});
-            }
-            else if (!features[featurePermissionedDomains])
-            {
-                // tries to set DomainID when PD is disabled
+                // tries to set DomainID when SAV or PD is disabled
                 mptAlice.create(
                     {.maxAmt = 100,
                      .assetScale = 0,
diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp
index 00667dc5ac..140fe2de15 100644
--- a/src/test/app/NFTokenBurn_test.cpp
+++ b/src/test/app/NFTokenBurn_test.cpp
@@ -198,6 +198,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
         // Use a default initialized mersenne_twister because we want the
         // effect of random numbers, but we want the test to run the same
         // way each time.
+        // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
         std::mt19937 engine;
         std::uniform_int_distribution feeDist(
             decltype(kMaxTransferFee){}, kMaxTransferFee);
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index dd28c7ec6e..9b29442197 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -7896,7 +7896,10 @@ class Vault_test : public beast::unit_test::Suite
 
                 // Depositor deep freeze → self-withdraw blocked
                 env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
+                // TODO: branches are identical - confirm the intended pre/post-fix330
+                // expectations and replace with the correct values (one branch may be wrong).
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    // NOLINTNEXTLINE(bugprone-branch-clone)
                     Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN)));
                 env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
 
diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp
index de80444f2e..36c1a08144 100644
--- a/src/test/app/XChain_test.cpp
+++ b/src/test/app/XChain_test.cpp
@@ -2164,14 +2164,7 @@ struct XChain_test : public beast::unit_test::Suite, public jtx::XChainBridgeObj
                     scAttester, jvb, mcAlice, amt, payees[i], true, claimID, dst, signers[i]);
 
                 TER const expectedTER = i < quorum ? tesSUCCESS : TER{tecXCHAIN_NO_CLAIM_ID};
-                if (i + 1 == quorum)
-                {
-                    scEnv.tx(att, Ter(expectedTER)).close();
-                }
-                else
-                {
-                    scEnv.tx(att, Ter(expectedTER)).close();
-                }
+                scEnv.tx(att, Ter(expectedTER)).close();
 
                 if (i + 1 < quorum)
                 {
@@ -4375,6 +4368,7 @@ public:
     {
         using namespace jtx;
         uint64_t time = 0;
+        // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
         std::mt19937 gen(27);  // Standard mersenne_twister_engine
         std::uniform_int_distribution distrib(0, 9);
 
diff --git a/src/test/consensus/LedgerTrie_test.cpp b/src/test/consensus/LedgerTrie_test.cpp
index 2b638b1dfa..0ddf1bf82f 100644
--- a/src/test/consensus/LedgerTrie_test.cpp
+++ b/src/test/consensus/LedgerTrie_test.cpp
@@ -663,6 +663,7 @@ class LedgerTrie_test : public beast::unit_test::Suite
         std::uint32_t const iterations = 10000;
 
         // Use explicit seed to have same results for CI
+        // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
         std::mt19937 gen{42};
         std::uniform_int_distribution<> depthDist(0, depthConst - 1);
         std::uniform_int_distribution<> widthDist(0, width - 1);
diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h
index 89e6b95fe6..b5b7f5f9c2 100644
--- a/src/test/csf/Sim.h
+++ b/src/test/csf/Sim.h
@@ -65,6 +65,7 @@ public:
         and no network connections.
 
     */
+    // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test
     Sim() : sink{scheduler.clock()}, j{sink}, net{scheduler}
     {
     }
diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h
index c35042bd4f..885c3bbeac 100644
--- a/src/test/nodestore/TestBase.h
+++ b/src/test/nodestore/TestBase.h
@@ -73,9 +73,7 @@ public:
                     case 2:
                         return NodeObjectType::TransactionNode;
                     case 3:
-                        return NodeObjectType::Unknown;
                     default:
-                        // will never happen, but make static analysis tool happy.
                         return NodeObjectType::Unknown;
                 }
             }();
diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp
index 14908cf72a..59dbf618f0 100644
--- a/src/test/rpc/LedgerEntry_test.cpp
+++ b/src/test/rpc/LedgerEntry_test.cpp
@@ -133,7 +133,6 @@ getTypeName(FieldType typeID)
         case FieldType::TwoAccountArrayField:
             return "length-2 array of Accounts";
         case FieldType::UInt32Field:
-            return "number";
         case FieldType::UInt64Field:
             return "number";
         default:
@@ -308,7 +307,6 @@ class LedgerEntry_test : public beast::unit_test::Suite
             case FieldType::TwoAccountArrayField:
                 return kTwoAccountArray;
             case FieldType::UInt32Field:
-                return 1;
             case FieldType::UInt64Field:
                 return 1;
             default:
diff --git a/src/test/unit_test/SuiteJournal.h b/src/test/unit_test/SuiteJournal.h
index bd54683031..174e9ff2ff 100644
--- a/src/test/unit_test/SuiteJournal.h
+++ b/src/test/unit_test/SuiteJournal.h
@@ -60,9 +60,8 @@ SuiteJournalSink::writeAlways(beast::Severity level, std::string const& text)
                 return "WRN:";
             case Severity::Error:
                 return "ERR:";
-            default:
-                break;
             case Severity::Fatal:
+            default:
                 break;
         }
         return "FTL:";
diff --git a/src/tests/libxrpl/tx/AccountSet.cpp b/src/tests/libxrpl/tx/AccountSet.cpp
index 46b298dde8..87d00c58bf 100644
--- a/src/tests/libxrpl/tx/AccountSet.cpp
+++ b/src/tests/libxrpl/tx/AccountSet.cpp
@@ -685,6 +685,7 @@ TEST(AccountSet, Gateway)
     IOU const usd("USD", gw);
 
     // Test gateway with a variety of allowed transfer rates
+    // NOLINTNEXTLINE(bugprone-float-loop-counter)
     for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125)
     {
         TxTest env;
diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp
index 1b2449823e..523cde743a 100644
--- a/src/xrpld/core/detail/Config.cpp
+++ b/src/xrpld/core/detail/Config.cpp
@@ -1006,13 +1006,9 @@ Config::loadFromString(std::string const& fileContents)
 
             if (!validatorsFile.empty())
             {
-                if (!boost::filesystem::exists(validatorsFile))
-                {
-                    validatorsFile.clear();
-                }
-                else if (
-                    !boost::filesystem::is_regular_file(validatorsFile) &&
-                    !boost::filesystem::is_symlink(validatorsFile))
+                if (!boost::filesystem::exists(validatorsFile) ||
+                    (!boost::filesystem::is_regular_file(validatorsFile) &&
+                     !boost::filesystem::is_symlink(validatorsFile)))
                 {
                     validatorsFile.clear();
                 }
diff --git a/src/xrpld/rpc/detail/PathRequestManager.cpp b/src/xrpld/rpc/detail/PathRequestManager.cpp
index ba42a0122f..7013353cb1 100644
--- a/src/xrpld/rpc/detail/PathRequestManager.cpp
+++ b/src/xrpld/rpc/detail/PathRequestManager.cpp
@@ -125,7 +125,8 @@ PathRequestManager::updateAll(std::shared_ptr const& inLedger)
                             json::Value update = request->doUpdate(cache, false, continueCallback);
                             request->updateComplete();
                             update[jss::type] = "path_find";
-                            if ((ipSub = getSubscriber(request)))
+                            ipSub = getSubscriber(request);
+                            if (ipSub)
                             {
                                 ipSub->send(update, false);
                                 remove = false;
diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp
index d7566622e8..fc788dea7d 100644
--- a/src/xrpld/rpc/detail/Pathfinder.cpp
+++ b/src/xrpld/rpc/detail/Pathfinder.cpp
@@ -646,21 +646,17 @@ Pathfinder::getBestPaths(
         {
             usePath = true;
         }
-        else if (extraPathsIterator->quality < pathsIterator->quality)
+        else if (extraPathsIterator->quality != pathsIterator->quality)
         {
-            useExtraPath = true;
+            // Prefer the lower (better) quality value
+            useExtraPath = extraPathsIterator->quality < pathsIterator->quality;
+            usePath = !useExtraPath;
         }
-        else if (extraPathsIterator->quality > pathsIterator->quality)
+        else if (extraPathsIterator->liquidity != pathsIterator->liquidity)
         {
-            usePath = true;
-        }
-        else if (extraPathsIterator->liquidity > pathsIterator->liquidity)
-        {
-            useExtraPath = true;
-        }
-        else if (extraPathsIterator->liquidity < pathsIterator->liquidity)
-        {
-            usePath = true;
+            // Equal quality: prefer the higher liquidity
+            useExtraPath = extraPathsIterator->liquidity > pathsIterator->liquidity;
+            usePath = !useExtraPath;
         }
         else
         {
@@ -795,31 +791,22 @@ Pathfinder::getPathsOut(
                     for (auto const& rspEntry : *lines)
                     {
                         if (pathAsset.get() != rspEntry.getLimit().get().currency)
-                        {
-                        }
-                        else if (
-                            rspEntry.getBalance() <= beast::kZero &&
+                            continue;
+                        if (rspEntry.getBalance() <= beast::kZero &&
                             (!rspEntry.getLimitPeer() ||
                              -rspEntry.getBalance() >= rspEntry.getLimitPeer() ||
                              (bAuthRequired && !rspEntry.getAuth())))
-                        {
-                        }
-                        else if (isDstAsset && dstAccount == rspEntry.getAccountIDPeer())
+                            continue;
+                        if (isDstAsset && dstAccount == rspEntry.getAccountIDPeer())
                         {
                             count += 10000;  // count a path to the destination extra
+                            continue;
                         }
-                        else if (rspEntry.getNoRipplePeer())
-                        {
-                            // This probably isn't a useful path out
-                        }
-                        else if (rspEntry.getFreezePeer())
-                        {
-                            // Not a useful path out
-                        }
-                        else
-                        {
-                            ++count;
-                        }
+                        if (rspEntry.getNoRipplePeer())
+                            continue;  // This probably isn't a useful path out
+                        if (rspEntry.getFreezePeer())
+                            continue;  // Not a useful path out
+                        ++count;
                     }
                 }
             },
@@ -828,26 +815,17 @@ Pathfinder::getPathsOut(
                 {
                     for (auto const& mpt : *mpts)
                     {
-                        if (pathAsset.get() != mpt.getMptID())
-                        {
-                        }
-                        else if (mpt.isZeroBalance() || mpt.isMaxedOut())
-                        {
-                        }
-                        else if (bAuthRequired)
-                        {
-                        }
-                        else if (isDstAsset && dstAccount == getMPTIssuer(mpt))
+                        if (pathAsset.get() != mpt.getMptID() || mpt.isZeroBalance() ||
+                            mpt.isMaxedOut() || bAuthRequired)
+                            continue;
+                        if (isDstAsset && dstAccount == getMPTIssuer(mpt))
                         {
                             count += 10000;
+                            continue;
                         }
-                        else if (isIndividualFrozen(*ledger_, account, MPTIssue{mpt.getMptID()}))
-                        {
-                        }
-                        else
-                        {
-                            ++count;
-                        }
+                        if (isIndividualFrozen(*ledger_, account, MPTIssue{mpt.getMptID()}))
+                            continue;
+                        ++count;
                     }
                 }
             });
@@ -1117,8 +1095,9 @@ Pathfinder::addLink(
                             if (checkAsset())
                             {
                                 // Can't leave on this path
+                                continue;
                             }
-                            else if (bToDestination)
+                            if (bToDestination)
                             {
                                 // destination is always worth trying
                                 if (uEndPathAsset == dstAmount_.asset())

From 809a6290752e4bfb484c602bf07beb9d19d898fb Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Mon, 29 Jun 2026 14:21:14 +0100
Subject: [PATCH 023/100] chore: Add a script to nicely format clang-tidy
 output (#7650)

---
 .github/workflows/reusable-clang-tidy.yml |  10 +--
 bin/filter-clang-tidy.py                  | 102 ++++++++++++++++++++++
 2 files changed, 107 insertions(+), 5 deletions(-)
 create mode 100755 bin/filter-clang-tidy.py

diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml
index e66909ffad..68ddea3ea2 100644
--- a/.github/workflows/reusable-clang-tidy.yml
+++ b/.github/workflows/reusable-clang-tidy.yml
@@ -96,10 +96,10 @@ jobs:
           set -o pipefail
           run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}"
 
-      - name: Print errors
+      - name: Print filtered clang-tidy errors
         if: ${{ steps.run_clang_tidy.outcome != 'success' }}
         run: |
-          sed '/error\||/!d' "${OUTPUT_FILE}"
+          bin/filter-clang-tidy.py "${OUTPUT_FILE}"
 
       - name: Upload clang-tidy output
         if: ${{ github.event.repository.visibility == 'public' && steps.run_clang_tidy.outcome != 'success' }}
@@ -143,12 +143,12 @@ jobs:
           \`\`\`
           EOF
 
-      - name: Append clang-tidy output to issue body (filter for errors and warnings)
+      - name: Append filtered clang-tidy output to issue body
         if: ${{ steps.run_clang_tidy.outcome != 'success' }}
         run: |
           if [ -f "${OUTPUT_FILE}" ]; then
-              # Extract lines containing 'error:', 'warning:', or 'note:'
-              grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true
+              # Filter to the unique errors with their source context.
+              bin/filter-clang-tidy.py "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true
 
               # If filtered output is empty, use original (might be a different error format)
               if [ ! -s "${FILTERED_OUTPUT_FILE}" ]; then
diff --git a/bin/filter-clang-tidy.py b/bin/filter-clang-tidy.py
new file mode 100755
index 0000000000..204a4e36f3
--- /dev/null
+++ b/bin/filter-clang-tidy.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+
+"""
+Reduce run-clang-tidy output to its unique errors.
+
+It does two things:
+
+  1. Filters the raw output down to diagnostics and their source-context lines
+     (the indented "  103 | ..." / "      | ^" lines clang-tidy prints),
+     matching the "path:line:col: error:" diagnostic shape.
+
+  2. Deduplicates. The same diagnostic in a header is reported once per
+     translation unit that includes it, so identical error blocks are collapsed
+     to their first occurrence.
+
+An "error block" is an "error:" line together with the indented context lines
+and any "note:" lines that follow it (up to the next "error:" line). Blocks are
+compared as a whole, so an error stays attached to its own context, and
+first-occurrence order is preserved.
+
+The deduplicated output goes to stdout; a summary of unique error counts per
+check is printed to stderr.
+
+Usage:
+  bin/filter-clang-tidy.py [INPUT_FILE]            # read from file, or
+  run-clang-tidy ... | bin/filter-clang-tidy.py    # read from stdin
+"""
+
+import re
+import sys
+from collections import Counter
+
+# A clang-tidy diagnostic line looks like "path:line:col: error: msg [check]".
+# Matching on that shape (rather than a loose "error" substring) avoids treating
+# progress lines whose paths contain "error" as diagnostics, e.g.
+#   [284/850][0.7s] /nix/.../clang-tidy ... src/.../error.cpp
+DIAG_RE = re.compile(r":\d+:\d+: (?:error|warning|note):")
+ERROR_RE = re.compile(r":\d+:\d+: error:")
+CHECK_RE = re.compile(r" error: .*\[([^\],]+)")
+
+
+def filter_and_dedup(lines: list[str]) -> list[str]:
+    """Keep diagnostics with their context, then drop duplicate error blocks."""
+    blocks: list[str] = []
+    seen: set[str] = set()
+    current: list[str] = []
+
+    def flush() -> None:
+        if not current:
+            return
+        block = "".join(current)
+        if block not in seen:
+            seen.add(block)
+            blocks.append(block)
+
+    for line in lines:
+        # Keep only diagnostics and their indented source-context lines; drop
+        # progress/status output and blank lines.
+        if not (DIAG_RE.search(line) or line[:1] in (" ", "\t")):
+            continue
+        # An "error:" line starts a new block; its context and any following
+        # "note:" lines (and their context) belong to it.
+        if ERROR_RE.search(line):
+            flush()
+            current = []
+        current.append(line)
+    flush()
+
+    return blocks
+
+
+def summarize(blocks: list[str]) -> Counter[str]:
+    """Count unique errors per check name (e.g. "bugprone-branch-clone")."""
+    counts: Counter[str] = Counter()
+    for block in blocks:
+        # The error line is the first line of the block.
+        match = CHECK_RE.search(block.splitlines()[0])
+        if match:
+            counts[match.group(1)] += 1
+    return counts
+
+
+def main() -> int:
+    if len(sys.argv) > 1 and sys.argv[1] != "-":
+        with open(sys.argv[1], encoding="utf-8") as f:
+            lines = f.readlines()
+    else:
+        lines = sys.stdin.readlines()
+
+    blocks = filter_and_dedup(lines)
+    # Blank line between blocks so distinct errors are easy to tell apart.
+    sys.stdout.write("\n".join(blocks))
+
+    print("\nUnique errors per check:", file=sys.stderr)
+    for check, count in summarize(blocks).most_common():
+        print(f"{count:>4} {check}", file=sys.stderr)
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

From 62bfc4ca5b8ef0dcfc4d5e44199b254fc278ef01 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Tue, 30 Jun 2026 11:43:21 +0100
Subject: [PATCH 024/100] build: Mark sec256k1 and mpt-crypto as transitive
 headers (#7658)

---
 conanfile.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/conanfile.py b/conanfile.py
index aec4f9eab0..db12dcb585 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -30,10 +30,8 @@ class Xrpl(ConanFile):
         "ed25519/2015.03",
         "grpc/1.81.1",
         "libarchive/3.8.7",
-        "mpt-crypto/0.4.0-rc2",
         "nudb/2.0.9",
         "openssl/3.6.3",
-        "secp256k1/0.7.1",
         "soci/4.0.3",
         "zlib/1.3.2",
     ]
@@ -133,13 +131,15 @@ class Xrpl(ConanFile):
     def requirements(self):
         self.requires("boost/1.91.0", force=True, transitive_headers=True)
         self.requires("date/3.0.4", transitive_headers=True)
-        self.requires("lz4/1.10.0", force=True)
-        self.requires("protobuf/6.33.5", force=True)
-        self.requires("sqlite3/3.53.0", force=True)
         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-rc2", transitive_headers=True)
+        self.requires("protobuf/6.33.5", force=True)
         if self.options.rocksdb:
             self.requires("rocksdb/10.5.1")
+        self.requires("secp256k1/0.7.1", transitive_headers=True)
+        self.requires("sqlite3/3.53.0", force=True)
         self.requires("xxhash/0.8.3", transitive_headers=True)
 
     exports_sources = (

From 95d53b4d43236ef9143aadc69c98a5e3c9b3b309 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Tue, 30 Jun 2026 11:43:44 +0100
Subject: [PATCH 025/100] ci: Use macOS 26 Tahoe with apple-clang 21 (#7601)

---
 .github/scripts/strategy-matrix/macos.json       | 2 +-
 .github/workflows/publish-docs.yml               | 2 +-
 .github/workflows/reusable-build-test-config.yml | 2 +-
 .github/workflows/reusable-clang-tidy.yml        | 2 +-
 .github/workflows/upload-conan-deps.yml          | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json
index 66d7a55a43..2d3cc75c7b 100644
--- a/.github/scripts/strategy-matrix/macos.json
+++ b/.github/scripts/strategy-matrix/macos.json
@@ -1,6 +1,6 @@
 {
   "platform": "macos/arm64",
-  "runner": ["self-hosted", "macOS", "ARM64", "mac-runner-m1"],
+  "runner": ["self-hosted", "macOS", "ARM64", "macos-26-apple-clang-21"],
   "configs": [
     {
       "build_type": "Release",
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
index cb7d4c5382..bfa8d2e79c 100644
--- a/.github/workflows/publish-docs.yml
+++ b/.github/workflows/publish-docs.yml
@@ -47,7 +47,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
+        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
         with:
           enable_ccache: false
 
diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml
index a81d9aec67..0cb0219d72 100644
--- a/.github/workflows/reusable-build-test-config.yml
+++ b/.github/workflows/reusable-build-test-config.yml
@@ -113,7 +113,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
+        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
         with:
           enable_ccache: ${{ inputs.ccache_enabled }}
 
diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml
index 68ddea3ea2..b04847e137 100644
--- a/.github/workflows/reusable-clang-tidy.yml
+++ b/.github/workflows/reusable-clang-tidy.yml
@@ -43,7 +43,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
+        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
         with:
           enable_ccache: false
 
diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml
index 92b72cf6a9..88b364c2b1 100644
--- a/.github/workflows/upload-conan-deps.yml
+++ b/.github/workflows/upload-conan-deps.yml
@@ -68,7 +68,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8
+        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
         with:
           enable_ccache: false
 

From 8abbd1ba3a194a652a158fcb6f3a0e9a13e4c9c1 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Tue, 30 Jun 2026 12:03:19 +0100
Subject: [PATCH 026/100] chore: Use std::ranges where possible (#7634)

---
 src/libxrpl/tx/ApplyContext.cpp               |  3 ++-
 src/libxrpl/tx/invariants/InvariantCheck.cpp  |  7 +++---
 .../tx/transactors/bridge/XChainBridge.cpp    |  7 +++---
 src/test/app/AmendmentTable_test.cpp          |  2 +-
 src/test/app/Manifest_test.cpp                |  7 +++---
 src/test/app/OfferMPT_test.cpp                | 14 +++++------
 src/test/app/XChain_test.cpp                  |  5 ++--
 .../beast/aged_associative_container_test.cpp | 20 ++++------------
 .../beast/beast_io_latency_probe_test.cpp     |  9 +++-----
 .../consensus/RCLCensorshipDetector_test.cpp  |  4 ++--
 src/test/csf/random.h                         |  2 +-
 src/test/peerfinder/Livecache_test.cpp        | 14 +++++------
 src/xrpld/app/ledger/detail/InboundLedger.cpp |  5 ++--
 src/xrpld/consensus/LedgerTrie.h              |  6 ++---
 src/xrpld/consensus/Validations.h             | 23 +++++++++----------
 src/xrpld/peerfinder/detail/Handouts.h        |  6 ++---
 src/xrpld/peerfinder/detail/Logic.h           | 18 +++++++--------
 src/xrpld/rpc/detail/ServerHandler.cpp        |  5 ++--
 18 files changed, 64 insertions(+), 93 deletions(-)

diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp
index 93b0d101af..5e5ab90441 100644
--- a/src/libxrpl/tx/ApplyContext.cpp
+++ b/src/libxrpl/tx/ApplyContext.cpp
@@ -14,6 +14,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -114,7 +115,7 @@ ApplyContext::checkInvariantsHelper(
             tx, result, fee, *view_, journal)...}};  // NOLINT(bugprone-unchecked-optional-access)
 
         // call each check's finalizer to see that it passes
-        if (!std::all_of(finalizers.cbegin(), finalizers.cend(), [](auto const& b) { return b; }))
+        if (!std::ranges::all_of(finalizers, [](auto const& b) { return b; }))
         {
             JLOG(journal.fatal()) << "Transaction has failed one or more global invariants: "
                                   << to_string(tx.getJson(JsonOptions::Values::None));
diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp
index 231705efaf..308342da74 100644
--- a/src/libxrpl/tx/invariants/InvariantCheck.cpp
+++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp
@@ -876,10 +876,9 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const
             {
                 std::vector const& fields = getPseudoAccountFields();
 
-                auto const numFields =
-                    std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool {
-                        return after->isFieldPresent(*sf);
-                    });
+                auto const numFields = std::ranges::count_if(
+                    fields,
+                    [&after](SField const* sf) -> bool { return after->isFieldPresent(*sf); });
                 if (numFields != 1)
                 {
                     std::stringstream error;
diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp
index 1e9e0bfc61..9092ae4140 100644
--- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp
+++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp
@@ -38,6 +38,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -301,10 +302,8 @@ onNewAttestations(
         }
 
         auto const& claimSigningAccount = att->attestationSignerAccount;
-        if (auto i = std::find_if(
-                attestations.begin(),
-                attestations.end(),
-                [&](auto const& a) { return a.keyAccount == claimSigningAccount; });
+        if (auto i = std::ranges::find_if(
+                attestations, [&](auto const& a) { return a.keyAccount == claimSigningAccount; });
             i != attestations.end())
         {
             // existing attestation
diff --git a/src/test/app/AmendmentTable_test.cpp b/src/test/app/AmendmentTable_test.cpp
index 7c2087bd4a..d420990cbc 100644
--- a/src/test/app/AmendmentTable_test.cpp
+++ b/src/test/app/AmendmentTable_test.cpp
@@ -140,7 +140,7 @@ private:
     combineArg(std::vector& dest, std::vector const& src, Args const&... args)
     {
         assert(dest.capacity() >= dest.size() + src.size());
-        std::copy(src.begin(), src.end(), std::back_inserter(dest));
+        std::ranges::copy(src, std::back_inserter(dest));
         if constexpr (sizeof...(args) > 0)
             combineArg(dest, args...);
     }
diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp
index 0cf1155cf5..50e8ab4a8d 100644
--- a/src/test/app/Manifest_test.cpp
+++ b/src/test/app/Manifest_test.cpp
@@ -290,10 +290,9 @@ public:
                 if (inManifests.size() == loadedManifests.size())
                 {
                     BEAST_EXPECT(
-                        std::equal(
-                            inManifests.begin(),
-                            inManifests.end(),
-                            loadedManifests.begin(),
+                        std::ranges::equal(
+                            inManifests,
+                            loadedManifests,
                             [](Manifest const* lhs, Manifest const* rhs) { return *lhs == *rhs; }));
                 }
                 else
diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp
index ac50924ac2..9958515c5f 100644
--- a/src/test/app/OfferMPT_test.cpp
+++ b/src/test/app/OfferMPT_test.cpp
@@ -3728,10 +3728,9 @@ public:
                     auto actorOffers = offersOnAccount(env, actor.acct);
                     auto const offerCount = std::distance(
                         actorOffers.begin(),
-                        std::remove_if(
-                            actorOffers.begin(), actorOffers.end(), [](SLE::const_pointer& offer) {
-                                return (*offer)[sfTakerGets].signum() == 0;
-                            }));
+                        std::ranges::remove_if(actorOffers, [](SLE::const_pointer& offer) {
+                            return (*offer)[sfTakerGets].signum() == 0;
+                        }).begin());
                     BEAST_EXPECT(offerCount == actor.offers);
 
                     env.require(Balance(actor.acct, actor.xrp));
@@ -3898,10 +3897,9 @@ public:
                     auto actorOffers = offersOnAccount(env, actor.acct);
                     auto const offerCount = std::distance(
                         actorOffers.begin(),
-                        std::remove_if(
-                            actorOffers.begin(), actorOffers.end(), [](SLE::const_pointer& offer) {
-                                return (*offer)[sfTakerGets].signum() == 0;
-                            }));
+                        std::ranges::remove_if(actorOffers, [](SLE::const_pointer& offer) {
+                            return (*offer)[sfTakerGets].signum() == 0;
+                        }).begin());
                     BEAST_EXPECT(offerCount == actor.offers);
 
                     env.require(Balance(actor.acct, actor.xrp));
diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp
index 36c1a08144..437c329e02 100644
--- a/src/test/app/XChain_test.cpp
+++ b/src/test/app/XChain_test.cpp
@@ -306,9 +306,8 @@ struct BalanceTransfer
     [[nodiscard]] bool
     payeesReceived(STAmount const& reward) const
     {
-        return std::all_of(rewardAccounts.begin(), rewardAccounts.end(), [&](balance const& b) {
-            return b.diff() == reward;
-        });
+        return std::ranges::all_of(
+            rewardAccounts, [&](balance const& b) { return b.diff() == reward; });
     }
 
     bool
diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp
index 2ac5fc5a33..413194491a 100644
--- a/src/test/beast/aged_associative_container_test.cpp
+++ b/src/test/beast/aged_associative_container_test.cpp
@@ -1296,13 +1296,7 @@ AgedAssociativeContainerTestBase::testChronological()
 
     typename Traits::template Cont<> c(v.begin(), v.end(), clock);
 
-    BEAST_EXPECT(
-        std::equal(
-            c.chronological.cbegin(),
-            c.chronological.cend(),
-            v.begin(),
-            v.end(),
-            EqualValue()));
+    BEAST_EXPECT(std::ranges::equal(c.chronological, v, EqualValue()));
 
     // Test touch() with a non-const iterator.
     for (auto iter(v.crbegin()); iter != v.crend(); ++iter)
@@ -1336,13 +1330,7 @@ AgedAssociativeContainerTestBase::testChronological()
         c.touch(found);
     }
 
-    BEAST_EXPECT(
-        std::equal(
-            c.chronological.cbegin(),
-            c.chronological.cend(),
-            v.cbegin(),
-            v.cend(),
-            EqualValue()));
+    BEAST_EXPECT(std::ranges::equal(c.chronological, v, EqualValue()));
 
     {
         // Because touch (reverse_iterator pos) is not allowed, the following
@@ -1407,8 +1395,8 @@ AgedAssociativeContainerTestBase::reverseFillAgedContainer(Container& c, Values
     clk.set(0);
 
     Values rev(values);
-    std::sort(rev.begin(), rev.end());
-    std::reverse(rev.begin(), rev.end());
+    std::ranges::sort(rev);
+    std::ranges::reverse(rev);
     for (auto& v : rev)
     {
         // Add values in reverse order so they are reversed chronologically.
diff --git a/src/test/beast/beast_io_latency_probe_test.cpp b/src/test/beast/beast_io_latency_probe_test.cpp
index b7e4980f05..9a93968dab 100644
--- a/src/test/beast/beast_io_latency_probe_test.cpp
+++ b/src/test/beast/beast_io_latency_probe_test.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include   // IWYU pragma: keep
 #include 
@@ -94,18 +95,14 @@ class io_latency_probe_test : public beast::unit_test::Suite, public beast::test
         auto
         getMax()
         {
-            return std::chrono::duration_cast(
-                       *std::max_element(elapsedTimes.begin(), elapsedTimes.end()))
-                .count();
+            return std::chrono::duration_cast(*std::ranges::max_element(elapsedTimes)).count();
         }
 
         template 
         auto
         getMin()
         {
-            return std::chrono::duration_cast(
-                       *std::min_element(elapsedTimes.begin(), elapsedTimes.end()))
-                .count();
+            return std::chrono::duration_cast(*std::ranges::min_element(elapsedTimes)).count();
         }
     };
 #endif
diff --git a/src/test/consensus/RCLCensorshipDetector_test.cpp b/src/test/consensus/RCLCensorshipDetector_test.cpp
index 32117e0089..722a34f937 100644
--- a/src/test/consensus/RCLCensorshipDetector_test.cpp
+++ b/src/test/consensus/RCLCensorshipDetector_test.cpp
@@ -31,12 +31,12 @@ class RCLCensorshipDetector_test : public beast::unit_test::Suite
         cdet.check(std::move(accepted), [&remove, &remain](auto id, auto seq) {
             // If the item is supposed to be removed from the censorship
             // detector internal tracker manually, do it now:
-            if (std::find(remove.begin(), remove.end(), id) != remove.end())
+            if (std::ranges::find(remove, id) != remove.end())
                 return true;
 
             // If the item is supposed to still remain in the censorship
             // detector internal tracker; remove it from the vector.
-            auto it = std::find(remain.begin(), remain.end(), id);
+            auto it = std::ranges::find(remain, id);
             if (it != remain.end())
                 remain.erase(it);
             return false;
diff --git a/src/test/csf/random.h b/src/test/csf/random.h
index 30f98e37fe..c506397d88 100644
--- a/src/test/csf/random.h
+++ b/src/test/csf/random.h
@@ -44,7 +44,7 @@ std::vector
 sample(std::size_t size, RandomNumberDistribution dist, Generator& g)
 {
     std::vector res(size);
-    std::generate(res.begin(), res.end(), [&dist, &g]() { return dist(g); });
+    std::ranges::generate(res, [&dist, &g]() { return dist(g); });
     return res;
 }
 
diff --git a/src/test/peerfinder/Livecache_test.cpp b/src/test/peerfinder/Livecache_test.cpp
index 9dae410b5b..4f2d6e97e1 100644
--- a/src/test/peerfinder/Livecache_test.cpp
+++ b/src/test/peerfinder/Livecache_test.cpp
@@ -167,10 +167,9 @@ public:
         for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end();
              ++i.first, ++i.second)
         {
-            std::copy((*i.second).begin(), (*i.second).end(), std::back_inserter(before[i.first]));
-            std::copy(
-                (*i.second).begin(), (*i.second).end(), std::back_inserter(beforeSorted[i.first]));
-            std::sort(beforeSorted[i.first].begin(), beforeSorted[i.first].end(), cmpEp);
+            std::ranges::copy(*i.second, std::back_inserter(before[i.first]));
+            std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first]));
+            std::ranges::sort(beforeSorted[i.first], cmpEp);
         }
 
         c.hops.shuffle();
@@ -180,10 +179,9 @@ public:
         for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end();
              ++i.first, ++i.second)
         {
-            std::copy((*i.second).begin(), (*i.second).end(), std::back_inserter(after[i.first]));
-            std::copy(
-                (*i.second).begin(), (*i.second).end(), std::back_inserter(afterSorted[i.first]));
-            std::sort(afterSorted[i.first].begin(), afterSorted[i.first].end(), cmpEp);
+            std::ranges::copy(*i.second, std::back_inserter(after[i.first]));
+            std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first]));
+            std::ranges::sort(afterSorted[i.first], cmpEp);
         }
 
         // each hop bucket should contain the same items
diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp
index e4df126ee8..4affffd1c1 100644
--- a/src/xrpld/app/ledger/detail/InboundLedger.cpp
+++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp
@@ -127,9 +127,8 @@ std::size_t
 InboundLedger::getPeerCount() const
 {
     auto const& peerIds = peerSet_->getPeerIds();
-    return std::count_if(peerIds.begin(), peerIds.end(), [this](auto id) {
-        return (app_.getOverlay().findPeerByShortID(id) != nullptr);
-    });
+    return std::ranges::count_if(
+        peerIds, [this](auto id) { return (app_.getOverlay().findPeerByShortID(id) != nullptr); });
 }
 
 void
diff --git a/src/xrpld/consensus/LedgerTrie.h b/src/xrpld/consensus/LedgerTrie.h
index cd9662ff02..b11a69a641 100644
--- a/src/xrpld/consensus/LedgerTrie.h
+++ b/src/xrpld/consensus/LedgerTrie.h
@@ -204,10 +204,8 @@ struct Node
     void
     erase(Node const* child)
     {
-        auto it = std::find_if(
-            children.begin(), children.end(), [child](std::unique_ptr const& curr) {
-                return curr.get() == child;
-            });
+        auto it = std::ranges::find_if(
+            children, [child](std::unique_ptr const& curr) { return curr.get() == child; });
         XRPL_ASSERT(it != children.end(), "xrpl::Node::erase : valid input");
         std::swap(*it, children.back());
         children.pop_back();
diff --git a/src/xrpld/consensus/Validations.h b/src/xrpld/consensus/Validations.h
index f109ae620b..d4da8a2887 100644
--- a/src/xrpld/consensus/Validations.h
+++ b/src/xrpld/consensus/Validations.h
@@ -817,16 +817,15 @@ public:
         if (!preferred)
         {
             // fall back to majority over acquiring ledgers
-            auto it = std::max_element(
-                acquiring_.begin(), acquiring_.end(), [](auto const& a, auto const& b) {
-                    std::pair const& aKey = a.first;
-                    typename hash_set::size_type const& aSize = a.second.size();
-                    std::pair const& bKey = b.first;
-                    typename hash_set::size_type const& bSize = b.second.size();
-                    // order by number of trusted peers validating that ledger
-                    // break ties with ledger ID
-                    return std::tie(aSize, aKey.second) < std::tie(bSize, bKey.second);
-                });
+            auto it = std::ranges::max_element(acquiring_, [](auto const& a, auto const& b) {
+                std::pair const& aKey = a.first;
+                typename hash_set::size_type const& aSize = a.second.size();
+                std::pair const& bKey = b.first;
+                typename hash_set::size_type const& bSize = b.second.size();
+                // order by number of trusted peers validating that ledger
+                // break ties with ledger ID
+                return std::tie(aSize, aKey.second) < std::tie(bSize, bKey.second);
+            });
             if (it != acquiring_.end())
                 return it->first;
             return std::nullopt;
@@ -896,7 +895,7 @@ public:
             return (preferred->first >= minSeq) ? preferred->second : lcl.id();
 
         // Otherwise, rely on peer ledgers
-        auto it = std::max_element(peerCounts.begin(), peerCounts.end(), [](auto& a, auto& b) {
+        auto it = std::ranges::max_element(peerCounts, [](auto const& a, auto const& b) {
             // Prefer larger counts, then larger ids on ties
             // (max_element expects this to return true if a < b)
             return std::tie(a.second, a.first) < std::tie(b.second, b.first);
@@ -932,7 +931,7 @@ public:
         }
 
         // Count parent ledgers as fallback
-        return std::count_if(lastLedger_.begin(), lastLedger_.end(), [&ledgerID](auto const& it) {
+        return std::ranges::count_if(lastLedger_, [&ledgerID](auto const& it) {
             auto const& curr = it.second;
             return curr.seq() > Seq{0} && curr[curr.seq() - Seq{1}] == ledgerID;
         });
diff --git a/src/xrpld/peerfinder/detail/Handouts.h b/src/xrpld/peerfinder/detail/Handouts.h
index 19a367f8c1..7523197c40 100644
--- a/src/xrpld/peerfinder/detail/Handouts.h
+++ b/src/xrpld/peerfinder/detail/Handouts.h
@@ -143,7 +143,7 @@ RedirectHandouts::tryInsert(Endpoint const& ep)
         return false;
 
     // Make sure the address isn't already in our list
-    if (std::any_of(list_.begin(), list_.end(), [&ep](Endpoint const& other) {
+    if (std::ranges::any_of(list_, [&ep](Endpoint const& other) {
             // Ignore port for security reasons
             return other.address.address() == ep.address.address();
         }))
@@ -222,7 +222,7 @@ SlotHandouts::tryInsert(Endpoint const& ep)
         return false;
 
     // Make sure the address isn't already in our list
-    if (std::any_of(list_.begin(), list_.end(), [&ep](Endpoint const& other) {
+    if (std::ranges::any_of(list_, [&ep](Endpoint const& other) {
             // Ignore port for security reasons
             return other.address.address() == ep.address.address();
         }))
@@ -311,7 +311,7 @@ ConnectHandouts::tryInsert(beast::IP::Endpoint const& endpoint)
         return false;
 
     // Make sure the address isn't already in our list
-    if (std::any_of(list_.begin(), list_.end(), [&endpoint](beast::IP::Endpoint const& other) {
+    if (std::ranges::any_of(list_, [&endpoint](beast::IP::Endpoint const& other) {
             // Ignore port for security reasons
             return other.address() == endpoint.address();
         }))
diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h
index 55cce506ba..b74643f7a5 100644
--- a/src/xrpld/peerfinder/detail/Logic.h
+++ b/src/xrpld/peerfinder/detail/Logic.h
@@ -573,19 +573,17 @@ public:
                 // build list of active slots
                 std::vector activeSlots;
                 activeSlots.reserve(slots.size());
-                std::for_each(
-                    slots.cbegin(), slots.cend(), [&activeSlots](Slots::value_type const& value) {
-                        if (value.second->state() == Slot::State::Active)
-                            activeSlots.emplace_back(value.second);
-                    });
+                std::ranges::for_each(slots, [&activeSlots](Slots::value_type const& value) {
+                    if (value.second->state() == Slot::State::Active)
+                        activeSlots.emplace_back(value.second);
+                });
                 std::shuffle(activeSlots.begin(), activeSlots.end(), defaultPrng());
 
                 // build target vector
                 targets.reserve(activeSlots.size());
-                std::for_each(
-                    activeSlots.cbegin(), activeSlots.cend(), [&targets](SlotImp::ptr const& slot) {
-                        targets.emplace_back(slot);
-                    });
+                std::ranges::for_each(activeSlots, [&targets](SlotImp::ptr const& slot) {
+                    targets.emplace_back(slot);
+                });
             }
 
             /* VFALCO NOTE
@@ -987,7 +985,7 @@ public:
         {
             auto const& address(iter->first.address());
             if (iter->second.when() <= now && squelches.find(address) == squelches.end() &&
-                std::none_of(slots.cbegin(), slots.cend(), [address](Slots::value_type const& v) {
+                std::ranges::none_of(slots, [address](Slots::value_type const& v) {
                     return address == v.first.address();
                 }))
             {
diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp
index 5177c85738..768cbf0dc0 100644
--- a/src/xrpld/rpc/detail/ServerHandler.cpp
+++ b/src/xrpld/rpc/detail/ServerHandler.cpp
@@ -1179,9 +1179,8 @@ parsePorts(Config const& config, std::ostream& log)
     }
     else
     {
-        auto const count = std::count_if(result.cbegin(), result.cend(), [](Port const& p) {
-            return p.protocol.contains("peer");
-        });
+        auto const count = std::ranges::count_if(
+            result, [](Port const& p) { return p.protocol.contains("peer"); });
 
         if (count > 1)
         {

From ecf7f805c90538e177470febb0a3a5b989c60620 Mon Sep 17 00:00:00 2001
From: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 1 Jul 2026 01:51:41 +0200
Subject: [PATCH 027/100] feat: Introduce lending 1.1 amendment and add
 `MemoData` field to `VaultDelete` transaction (#6324)

---
 include/xrpl/protocol/detail/features.macro   |  1 +
 .../xrpl/protocol/detail/transactions.macro   |  1 +
 .../transactions/VaultDelete.h                | 37 ++++++++++
 .../tx/transactors/vault/VaultDelete.cpp      |  8 +++
 src/test/app/Vault_test.cpp                   | 70 +++++++++++++++++++
 .../transactions/VaultDeleteTests.cpp         | 49 +++++++++++++
 6 files changed, 166 insertions(+)

diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index a9b237be32..3c808d960b 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -14,6 +14,7 @@
 
 // Add new amendments to the top of this list.
 // Keep it sorted in reverse chronological order.
+XRPL_FEATURE(LendingProtocolV1_1,         Supported::No, VoteBehavior::DefaultNo)
 XRPL_FEATURE(ConfidentialTransfer,        Supported::No, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_3_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_2_0,                Supported::Yes, VoteBehavior::DefaultNo)
diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro
index 8a3b0ff2ce..b46fca92b6 100644
--- a/include/xrpl/protocol/detail/transactions.macro
+++ b/include/xrpl/protocol/detail/transactions.macro
@@ -889,6 +889,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
     MustDeleteAcct | DestroyMptIssuance | MustModifyVault,
     ({
     {sfVaultID, SoeRequired},
+    {sfMemoData, SoeOptional},
 }))
 
 /** This transaction trades assets for shares with a vault. */
diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h b/include/xrpl/protocol_autogen/transactions/VaultDelete.h
index 2b6e248c0a..b4c08ae229 100644
--- a/include/xrpl/protocol_autogen/transactions/VaultDelete.h
+++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h
@@ -57,6 +57,32 @@ public:
     {
         return this->tx_->at(sfVaultID);
     }
+
+    /**
+     * @brief Get sfMemoData (SoeOptional)
+     * @return The field value, or std::nullopt if not present.
+     */
+    [[nodiscard]]
+    protocol_autogen::Optional
+    getMemoData() const
+    {
+        if (hasMemoData())
+        {
+            return this->tx_->at(sfMemoData);
+        }
+        return std::nullopt;
+    }
+
+    /**
+     * @brief Check if sfMemoData is present.
+     * @return True if the field is present, false otherwise.
+     */
+    [[nodiscard]]
+    bool
+    hasMemoData() const
+    {
+        return this->tx_->isFieldPresent(sfMemoData);
+    }
 };
 
 /**
@@ -112,6 +138,17 @@ public:
         return *this;
     }
 
+    /**
+     * @brief Set sfMemoData (SoeOptional)
+     * @return Reference to this builder for method chaining.
+     */
+    VaultDeleteBuilder&
+    setMemoData(std::decay_t const& value)
+    {
+        object_[sfMemoData] = value;
+        return *this;
+    }
+
     /**
      * @brief Build and return the VaultDelete wrapper.
      * @param publicKey The public key for signing.
diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
index 0550f08b17..fa38ae278b 100644
--- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
@@ -7,8 +7,10 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include   // IWYU pragma: keep
@@ -28,6 +30,12 @@ VaultDelete::preflight(PreflightContext const& ctx)
         return temMALFORMED;
     }
 
+    if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1))
+        return temDISABLED;
+
+    if (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength))
+        return temMALFORMED;
+
     return tesSUCCESS;
 }
 
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index 9b29442197..d9afd93b23 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -7511,6 +7511,74 @@ class Vault_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testVaultDeleteMemoData()
+    {
+        using namespace test::jtx;
+
+        Env env{*this};
+
+        Account const owner{"owner"};
+        env.fund(XRP(1'000'000), owner);
+        env.close();
+
+        Vault const vault{env};
+
+        auto const keylet = keylet::vault(owner.id(), 1);
+        auto delTx = vault.del({.owner = owner, .id = keylet.key});
+
+        // Test VaultDelete with featureLendingProtocolV1_1 disabled
+        // Transaction fails if the data field is provided
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 disabled");
+            env.disableFeature(featureLendingProtocolV1_1);
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
+            env(delTx, Ter(temDISABLED));
+            env.enableFeature(featureLendingProtocolV1_1);
+            env.close();
+        }
+
+        // Transaction fails if the data field is too large
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data too large");
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
+            env(delTx, Ter(temMALFORMED));
+            env.close();
+        }
+
+        // Transaction fails if the data field is set, but is empty
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty");
+            delTx[sfMemoData] = strHex(std::string(0, 'A'));
+            env(delTx, Ter(temMALFORMED));
+            env.close();
+        }
+
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault");
+            auto const keylet = keylet::vault(owner.id(), env.seq(owner));
+
+            // Recreate the transaction as the vault keylet changed
+            auto delTx = vault.del({.owner = owner, .id = keylet.key});
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
+            env(delTx, Ter(tecNO_ENTRY));
+            env.close();
+        }
+
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data valid");
+            PrettyAsset const xrpAsset = xrpIssue();
+            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+            // Recreate the transaction as the vault keylet changed
+            auto delTx = vault.del({.owner = owner, .id = keylet.key});
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
+            env(delTx, Ter(tesSUCCESS));
+            env.close();
+        }
+    }
+
     void
     testVaultDepositFreeze()
     {
@@ -8082,6 +8150,7 @@ class Vault_test : public beast::unit_test::Suite
 
             runTests();
             env.disableFeature(fixCleanup3_3_0);
+
             runTests();
             env.enableFeature(fixCleanup3_3_0);
         }
@@ -8115,6 +8184,7 @@ public:
         testVaultClawbackAssets();
         testVaultEscrowedMPT();
         testAssetsMaximum();
+        testVaultDeleteMemoData();
         testBug6LimitBypassWithShares();
         testRemoveEmptyHoldingLockedAmount();
         testRemoveEmptyHoldingConfidentialBalances();
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp
index 70747af1ed..8f0eb2e4cd 100644
--- a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp
@@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
 
     // Transaction-specific field values
     auto const vaultIDValue = canonical_UINT256();
+    auto const memoDataValue = canonical_VL();
 
     VaultDeleteBuilder builder{
         accountValue,
@@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
     };
 
     // Set optional fields
+    builder.setMemoData(memoDataValue);
 
     auto tx = builder.build(publicKey, secretKey);
 
@@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
     }
 
     // Verify optional fields
+    {
+        auto const& expected = memoDataValue;
+        auto const actualOpt = tx.getMemoData();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
+        expectEqualField(expected, *actualOpt, "sfMemoData");
+        EXPECT_TRUE(tx.hasMemoData());
+    }
+
 }
 
 // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
 
     // Transaction-specific field values
     auto const vaultIDValue = canonical_UINT256();
+    auto const memoDataValue = canonical_VL();
 
     // Build an initial transaction
     VaultDeleteBuilder initialBuilder{
@@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
         feeValue
     };
 
+    initialBuilder.setMemoData(memoDataValue);
 
     auto initialTx = initialBuilder.build(publicKey, secretKey);
 
@@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
     }
 
     // Verify optional fields
+    {
+        auto const& expected = memoDataValue;
+        auto const actualOpt = rebuiltTx.getMemoData();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
+        expectEqualField(expected, *actualOpt, "sfMemoData");
+    }
+
 }
 
 // 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType)
     EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
 }
 
+// 5) Build with only required fields and verify optional fields return nullopt.
+TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt)
+{
+    // Generate a deterministic keypair for signing
+    auto const [publicKey, secretKey] =
+        generateKeyPair(KeyType::Secp256k1, generateSeed("testVaultDeleteNullopt"));
+
+    // Common transaction fields
+    auto const accountValue = calcAccountID(publicKey);
+    std::uint32_t const sequenceValue = 3;
+    auto const feeValue = canonical_AMOUNT();
+
+    // Transaction-specific required field values
+    auto const vaultIDValue = canonical_UINT256();
+
+    VaultDeleteBuilder builder{
+        accountValue,
+        vaultIDValue,
+        sequenceValue,
+        feeValue
+    };
+
+    // Do NOT set optional fields
+
+    auto tx = builder.build(publicKey, secretKey);
+
+    // Verify optional fields are not present
+    EXPECT_FALSE(tx.hasMemoData());
+    EXPECT_FALSE(tx.getMemoData().has_value());
+}
 
 }

From 86d8b244d6e9b61952de736c010c789e12c6498e Mon Sep 17 00:00:00 2001
From: Denis Angell 
Date: Wed, 1 Jul 2026 08:47:14 -0400
Subject: [PATCH 028/100] feat: Add Batch (XLS-56) V1_1 (#6446)

Co-authored-by: Mayukha Vadari 
---
 include/xrpl/core/HashRouter.h                |    6 +-
 include/xrpl/ledger/OpenView.h                |    2 +-
 include/xrpl/protocol/Batch.h                 |   10 +-
 include/xrpl/protocol/Protocol.h              |    3 +
 include/xrpl/protocol/STObject.h              |    5 +
 include/xrpl/protocol/STTx.h                  |   53 +-
 include/xrpl/protocol/TER.h                   |    2 +
 include/xrpl/protocol/detail/features.macro   |    8 +-
 .../xrpl/protocol/detail/transactions.macro   |    2 +-
 .../protocol_autogen/transactions/Batch.h     |    2 +-
 include/xrpl/tx/ApplyContext.h                |    6 +-
 include/xrpl/tx/Transactor.h                  |   49 +-
 include/xrpl/tx/transactors/system/Batch.h    |    8 +-
 src/libxrpl/protocol/STObject.cpp             |   14 +
 src/libxrpl/protocol/STTx.cpp                 |  133 +-
 src/libxrpl/protocol/TER.cpp                  |    2 +
 src/libxrpl/tx/Transactor.cpp                 |  105 +-
 src/libxrpl/tx/apply.cpp                      |   34 +-
 .../tx/transactors/lending/LoanSet.cpp        |    2 +-
 .../tx/transactors/payment/Payment.cpp        |   32 +-
 src/libxrpl/tx/transactors/system/Batch.cpp   |  153 +-
 src/test/app/Batch_test.cpp                   | 1227 ++++++++++++++---
 .../app/ConfidentialTransferExtended_test.cpp |   35 +-
 src/test/app/LedgerReplay_test.cpp            |   45 +-
 src/test/app/TxQ_test.cpp                     |    3 +-
 src/test/jtx/TestHelpers.h                    |    4 +-
 src/test/jtx/batch.h                          |    5 +-
 src/test/jtx/impl/batch.cpp                   |   16 +-
 src/test/rpc/Feature_test.cpp                 |    2 +-
 src/test/rpc/Simulate_test.cpp                |   16 +-
 src/xrpld/app/ledger/detail/BuildLedger.cpp   |    9 +
 src/xrpld/app/misc/NetworkOPs.cpp             |    7 +-
 src/xrpld/app/misc/detail/TxQ.cpp             |    7 +
 src/xrpld/overlay/detail/PeerImp.cpp          |    4 +-
 .../rpc/handlers/transaction/Simulate.cpp     |    8 +
 35 files changed, 1536 insertions(+), 483 deletions(-)

diff --git a/include/xrpl/core/HashRouter.h b/include/xrpl/core/HashRouter.h
index d36b8aee6e..c8b34d8e93 100644
--- a/include/xrpl/core/HashRouter.h
+++ b/include/xrpl/core/HashRouter.h
@@ -19,12 +19,14 @@ enum class HashRouterFlags : std::uint16_t {
     HELD = 0x08,     // Held by LedgerMaster after potential processing failure
     TRUSTED = 0x10,  // Comes from a trusted source
 
-    // Private flags (used internally in apply.cpp)
-    // Do not attempt to read, set, or reuse.
+    // Private flags. Each group is owned by one file; do not read, set, or
+    // reuse a flag outside the file noted.
+    // Used in apply.cpp
     PRIVATE1 = 0x0100,
     PRIVATE2 = 0x0200,
     PRIVATE3 = 0x0400,
     PRIVATE4 = 0x0800,
+    // Used in EscrowFinish.cpp
     PRIVATE5 = 0x1000,
     PRIVATE6 = 0x2000
 };
diff --git a/include/xrpl/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h
index 4ba2a7759b..d145473516 100644
--- a/include/xrpl/ledger/OpenView.h
+++ b/include/xrpl/ledger/OpenView.h
@@ -28,7 +28,7 @@ inline constexpr struct OpenLedgerT
 /** Batch view construction tag.
 
     Views constructed with this tag are part of a stack of views
-    used during batch transaction applied.
+    used during batch transaction application.
  */
 inline constexpr struct BatchViewT
 {
diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h
index 2f2412b3ff..4e021442d6 100644
--- a/include/xrpl/protocol/Batch.h
+++ b/include/xrpl/protocol/Batch.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
@@ -7,9 +8,16 @@
 namespace xrpl {
 
 inline void
-serializeBatch(Serializer& msg, std::uint32_t const& flags, std::vector const& txids)
+serializeBatch(
+    Serializer& msg,
+    AccountID const& outerAccount,
+    std::uint32_t outerSeqValue,
+    std::uint32_t const& flags,
+    std::vector const& txids)
 {
     msg.add32(HashPrefix::Batch);
+    msg.addBitString(outerAccount);
+    msg.add32(outerSeqValue);
     msg.add32(flags);
     msg.add32(std::uint32_t(txids.size()));
     for (auto const& txid : txids)
diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h
index 7eac92e83c..ba6347dfa7 100644
--- a/include/xrpl/protocol/Protocol.h
+++ b/include/xrpl/protocol/Protocol.h
@@ -311,6 +311,9 @@ constexpr std::size_t kPermissionMaxSize = 10;
 /** The maximum number of transactions that can be in a batch. */
 constexpr std::size_t kMaxBatchTxCount = 8;
 
+/** The maximum number of batch signers. */
+constexpr std::size_t kMaxBatchSigners = kMaxBatchTxCount * 3;
+
 /** Length of a secp256k1 scalar in bytes. */
 constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE;
 
diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h
index e65cc79c78..044e48ce62 100644
--- a/include/xrpl/protocol/STObject.h
+++ b/include/xrpl/protocol/STObject.h
@@ -217,6 +217,11 @@ public:
     [[nodiscard]] AccountID
     getAccountID(SField const& field) const;
 
+    /** The account responsible for the fee and authorization: the delegate when
+        sfDelegate is present, otherwise the account. */
+    [[nodiscard]] AccountID
+    getFeePayer() const;
+
     [[nodiscard]] Blob
     getFieldVL(SField const& field) const;
     [[nodiscard]] STAmount const&
diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h
index 659fede31d..e78fce27a1 100644
--- a/include/xrpl/protocol/STTx.h
+++ b/include/xrpl/protocol/STTx.h
@@ -12,6 +12,7 @@
 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
@@ -51,51 +52,48 @@ public:
     STTx(TxType type, std::function assembler);
 
     // STObject functions.
-    SerializedTypeID
+    [[nodiscard]] SerializedTypeID
     getSType() const override;
 
-    std::string
+    [[nodiscard]] std::string
     getFullText() const override;
 
     // Outer transaction functions / signature functions.
     static Blob
     getSignature(STObject const& sigObject);
 
-    Blob
+    [[nodiscard]] Blob
     getSignature() const
     {
         return getSignature(*this);
     }
 
-    uint256
+    [[nodiscard]] uint256
     getSigningHash() const;
 
-    TxType
+    [[nodiscard]] TxType
     getTxnType() const;
 
-    Blob
+    [[nodiscard]] Blob
     getSigningPubKey() const;
 
-    SeqProxy
+    [[nodiscard]] SeqProxy
     getSeqProxy() const;
 
     /** Returns the first non-zero value of (Sequence, TicketSequence). */
-    std::uint32_t
+    [[nodiscard]] std::uint32_t
     getSeqValue() const;
 
-    AccountID
-    getFeePayer() const;
-
-    boost::container::flat_set
+    [[nodiscard]] boost::container::flat_set
     getMentionedAccounts() const;
 
-    uint256
+    [[nodiscard]] uint256
     getTransactionID() const;
 
-    json::Value
+    [[nodiscard]] json::Value
     getJson(JsonOptions options) const override;
 
-    json::Value
+    [[nodiscard]] json::Value
     getJson(JsonOptions options, bool binary) const;
 
     void
@@ -108,27 +106,27 @@ public:
         @param rules The current ledger rules.
         @return `true` if valid signature. If invalid, the error message string.
     */
-    std::expected
+    [[nodiscard]] std::expected
     checkSign(Rules const& rules) const;
 
-    std::expected
+    [[nodiscard]] std::expected
     checkBatchSign(Rules const& rules) const;
 
     // SQL Functions with metadata.
     static std::string const&
     getMetaSQLInsertReplaceHeader();
 
-    std::string
+    [[nodiscard]] std::string
     getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData) const;
 
-    std::string
+    [[nodiscard]] std::string
     getMetaSQL(
         Serializer rawTxn,
         std::uint32_t inLedger,
         TxnSql status,
         std::string const& escapedMetaData) const;
 
-    std::vector const&
+    [[nodiscard]] std::vector const&
     getBatchTransactionIDs() const;
 
 private:
@@ -138,28 +136,31 @@ private:
             Will be *this more often than not.
         @return `true` if valid signature. If invalid, the error message string.
     */
-    std::expected
+    [[nodiscard]] std::expected
     checkSign(Rules const& rules, STObject const& sigObject) const;
 
-    std::expected
+    [[nodiscard]] std::expected
     checkSingleSign(STObject const& sigObject) const;
 
-    std::expected
+    [[nodiscard]] std::expected
     checkMultiSign(Rules const& rules, STObject const& sigObject) const;
 
-    std::expected
+    [[nodiscard]] std::expected
     checkBatchSingleSign(STObject const& batchSigner) const;
 
-    std::expected
+    [[nodiscard]] std::expected
     checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
 
+    void
+    buildBatchTxnIds();
+
     STBase*
     copy(std::size_t n, void* buf) const override;
     STBase*
     move(std::size_t n, void* buf) override;
 
     friend class detail::STVar;
-    mutable std::vector batchTxnIds_;
+    std::optional> batchTxnIds_;
 };
 
 bool
diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h
index 84c344ea76..4ee084e714 100644
--- a/include/xrpl/protocol/TER.h
+++ b/include/xrpl/protocol/TER.h
@@ -175,6 +175,8 @@ enum TEFcodes : TERUnderlyingType {
     tefNO_TICKET,
     tefNFTOKEN_IS_NOT_TRANSFERABLE,
     tefINVALID_LEDGER_FIX_TYPE,
+    tefNO_DST_PARTIAL,
+    tefBAD_PATH_COUNT,
 };
 
 //------------------------------------------------------------------------------
diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index 3c808d960b..a1911761bb 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -14,13 +14,14 @@
 
 // Add new amendments to the top of this list.
 // Keep it sorted in reverse chronological order.
-XRPL_FEATURE(LendingProtocolV1_1,         Supported::No, VoteBehavior::DefaultNo)
-XRPL_FEATURE(ConfidentialTransfer,        Supported::No, VoteBehavior::DefaultNo)
+
+XRPL_FEATURE(BatchV1_1,                   Supported::No,  VoteBehavior::DefaultNo)
+XRPL_FEATURE(LendingProtocolV1_1,         Supported::No,  VoteBehavior::DefaultNo)
+XRPL_FEATURE(ConfidentialTransfer,        Supported::No,  VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_3_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_2_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(MPTokensV2,                  Supported::No,  VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_1_3,                Supported::Yes, VoteBehavior::DefaultYes)
-XRPL_FIX    (BatchInnerSigs,              Supported::No,  VoteBehavior::DefaultNo)
 XRPL_FEATURE(LendingProtocol,             Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(PermissionDelegationV1_1,    Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (DirectoryLimit,              Supported::Yes, VoteBehavior::DefaultNo)
@@ -34,7 +35,6 @@ XRPL_FEATURE(TokenEscrow,                 Supported::Yes, VoteBehavior::DefaultN
 XRPL_FIX    (EnforceNFTokenTrustlineV2,   Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (AMMv1_3,                     Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(PermissionedDEX,             Supported::Yes, VoteBehavior::DefaultNo)
-XRPL_FEATURE(Batch,                       Supported::No,  VoteBehavior::DefaultNo)
 XRPL_FEATURE(SingleAssetVault,            Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (PayChanCancelAfter,          Supported::Yes, VoteBehavior::DefaultNo)
 // Check flags in Credential transactions
diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro
index b46fca92b6..b0c4f66bae 100644
--- a/include/xrpl/protocol/detail/transactions.macro
+++ b/include/xrpl/protocol/detail/transactions.macro
@@ -940,7 +940,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
 #endif
 TRANSACTION(ttBATCH, 71, Batch,
     Delegation::NotDelegable,
-    featureBatch,
+    featureBatchV1_1,
     NoPriv,
     ({
     {sfRawTransactions, SoeRequired},
diff --git a/include/xrpl/protocol_autogen/transactions/Batch.h b/include/xrpl/protocol_autogen/transactions/Batch.h
index 55b99d0eef..00f553ada7 100644
--- a/include/xrpl/protocol_autogen/transactions/Batch.h
+++ b/include/xrpl/protocol_autogen/transactions/Batch.h
@@ -20,7 +20,7 @@ class BatchBuilder;
  *
  * Type: ttBATCH (71)
  * Delegable: Delegation::NotDelegable
- * Amendment: featureBatch
+ * Amendment: featureBatchV1_1
  * Privileges: NoPriv
  *
  * Immutable wrapper around STTx providing type-safe field access.
diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h
index 8540037601..c98b3f82c5 100644
--- a/include/xrpl/tx/ApplyContext.h
+++ b/include/xrpl/tx/ApplyContext.h
@@ -24,6 +24,10 @@ public:
         ApplyFlags flags,
         beast::Journal journal = beast::Journal{beast::Journal::getNullSink()});
 
+    // Convenience constructor used only by tests that build an ApplyContext
+    // directly (e.g. invariant checks). Production always uses the parentBatchId
+    // constructor above; this one fixes parentBatchId to std::nullopt and so is
+    // never valid for a batch inner (hence the TapBatch assert).
     explicit ApplyContext(
         ServiceRegistry& registry,
         OpenView& base,
@@ -124,7 +128,7 @@ private:
     ApplyFlags flags_;
     std::optional view_;
 
-    // The ID of the batch transaction we are executing under, if seated.
+    // The ID of the batch transaction we are executing under, if set.
     std::optional parentBatchId_;
 };
 
diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h
index 02fedfe970..2b50cfa6e7 100644
--- a/include/xrpl/tx/Transactor.h
+++ b/include/xrpl/tx/Transactor.h
@@ -180,9 +180,6 @@ public:
     static NotTEC
     checkSign(PreclaimContext const& ctx);
 
-    static NotTEC
-    checkBatchSign(PreclaimContext const& ctx);
-
     // Returns the fee in fee units, not scaled for load.
     static XRPAmount
     calculateBaseFee(ReadView const& view, STTx const& tx);
@@ -373,7 +370,12 @@ protected:
         std::optional const& parentBatchId,
         AccountID const& idAccount,
         STObject const& sigObject,
-        beast::Journal const j);
+        beast::Journal const j,
+        // A batch may carry an inner from an account that an earlier inner
+        // creates, so the signer account need not exist yet; when it does not,
+        // only its own master key may authorize it. Normal transactions require
+        // the account to already exist.
+        bool permitUncreatedAccount = false);
 
     // Base class always returns true
     static bool
@@ -413,25 +415,8 @@ protected:
         std::optional value,
         unit::ValueUnit min = unit::ValueUnit{});
 
-private:
-    static NotTEC
-    checkPermission(
-        ReadView const& view,
-        STTx const& tx,
-        std::unordered_set& heldGranularPermissions);
-
-    std::pair
-    reset(XRPAmount fee);
-
-    TER
-    consumeSeqProxy(SLE::pointer const& sleAccount);
-
-    TER
-    payFee();
-
-    std::tuple
-    processPersistentChanges(TER result, XRPAmount fee);
-
+    // Signature-authorization helpers. protected so the Batch transactor can
+    // reuse them when validating each BatchSigner in Batch::checkBatchSign.
     static NotTEC
     checkSingleSign(
         ReadView const& view,
@@ -448,6 +433,24 @@ private:
         STObject const& sigObject,
         beast::Journal const j);
 
+private:
+    static NotTEC
+    checkPermission(
+        ReadView const& view,
+        STTx const& tx,
+        std::unordered_set& heldGranularPermissions);
+
+    std::pair
+    reset(XRPAmount fee);
+
+    TER
+    consumeSeqProxy(SLE::pointer const& sleAccount);
+    TER
+    payFee();
+
+    std::tuple
+    processPersistentChanges(TER result, XRPAmount fee);
+
     void trapTransaction(uint256) const;
 
     /** Performs early sanity checks on the account and fee fields.
diff --git a/include/xrpl/tx/transactors/system/Batch.h b/include/xrpl/tx/transactors/system/Batch.h
index 43f0103319..927a5b27cd 100644
--- a/include/xrpl/tx/transactors/system/Batch.h
+++ b/include/xrpl/tx/transactors/system/Batch.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-#include 
 #include 
 
 namespace xrpl {
@@ -61,6 +59,12 @@ public:
         ttLOAN_MANAGE,
         ttLOAN_PAY,
     });
+
+private:
+    // Skips signature verification for inner txns, so keep it private: it must
+    // only be reached through Batch::checkSign.
+    static NotTEC
+    checkBatchSign(PreclaimContext const& ctx);
 };
 
 }  // namespace xrpl
diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp
index e16cbc871f..445da16828 100644
--- a/src/libxrpl/protocol/STObject.cpp
+++ b/src/libxrpl/protocol/STObject.cpp
@@ -635,6 +635,20 @@ STObject::getAccountID(SField const& field) const
     return getFieldByValue(field);
 }
 
+AccountID
+STObject::getFeePayer() const
+{
+    // If sfDelegate is present, the delegate account is the payer
+    // 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 payer
+    return getAccountID(sfAccount);
+}
+
 Blob
 STObject::getFieldVL(SField const& field) const
 {
diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp
index be3b1a082f..cd2da12316 100644
--- a/src/libxrpl/protocol/STTx.cpp
+++ b/src/libxrpl/protocol/STTx.cpp
@@ -1,7 +1,6 @@
 #include 
 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -72,6 +71,7 @@ STTx::STTx(STObject&& object) : STObject(std::move(object))
     txType_ = safeCast(getFieldU16(sfTransactionType));
     applyTemplate(getTxFormat(txType_)->getSOTemplate());  //  may throw
     tid_ = getHash(HashPrefix::TransactionId);
+    buildBatchTxnIds();
 }
 
 STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
@@ -88,6 +88,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
 
     applyTemplate(getTxFormat(txType_)->getSOTemplate());  // May throw
     tid_ = getHash(HashPrefix::TransactionId);
+    buildBatchTxnIds();
 }
 
 STTx::STTx(TxType type, std::function assembler) : STObject(sfTransaction)
@@ -105,6 +106,7 @@ STTx::STTx(TxType type, std::function assembler) : STObject(sfT
         logicError("Transaction type was mutated during assembly");
 
     tid_ = getHash(HashPrefix::TransactionId);
+    buildBatchTxnIds();
 }
 
 STBase*
@@ -212,20 +214,6 @@ STTx::getSeqValue() const
     return getSeqProxy().value();
 }
 
-AccountID
-STTx::getFeePayer() const
-{
-    // If sfDelegate is present, the delegate account is the payer
-    // 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 payer
-    return getAccountID(sfAccount);
-}
-
 void
 STTx::sign(
     PublicKey const& publicKey,
@@ -279,6 +267,17 @@ STTx::checkSign(Rules const& rules) const
         if (auto const ret = checkSign(rules, counterSig); !ret)
             return std::unexpected("Counterparty: " + ret.error());
     }
+
+    // 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;
+    }
     return {};
 }
 
@@ -287,12 +286,15 @@ STTx::checkBatchSign(Rules const& rules) const
 {
     try
     {
-        XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSign : not a batch transaction");
         if (getTxnType() != ttBATCH)
         {
-            JLOG(debugLog().fatal()) << "not a batch transaction";
+            // LCOV_EXCL_START
+            UNREACHABLE("STTx::checkBatchSign : not a batch transaction");
             return std::unexpected("Not a batch transaction.");
+            // LCOV_EXCL_STOP
         }
+        if (!isFieldPresent(sfBatchSigners))
+            return std::unexpected("Missing BatchSigners field.");  // LCOV_EXCL_LINE
         STArray const& signers{getFieldArray(sfBatchSigners)};
         for (auto const& signer : signers)
         {
@@ -307,9 +309,10 @@ STTx::checkBatchSign(Rules const& rules) const
     }
     catch (std::exception const& e)
     {
-        JLOG(debugLog().error()) << "Batch signature check failed: " << e.what();
+        // LCOV_EXCL_START
+        return std::unexpected(std::string("Internal batch signature check failure: ") + e.what());
+        // LCOV_EXCL_STOP
     }
-    return std::unexpected("Internal batch signature check failure.");
 }
 
 json::Value
@@ -429,8 +432,11 @@ STTx::checkSingleSign(STObject const& sigObject) const
 std::expected
 STTx::checkBatchSingleSign(STObject const& batchSigner) const
 {
+    XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction");
     Serializer msg;
-    serializeBatch(msg, getFlags(), getBatchTransactionIDs());
+    serializeBatch(
+        msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
+    finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
     return singleSignHelper(batchSigner, msg.slice());
 }
 
@@ -504,7 +510,7 @@ multiSignHelper(
         {
             return std::unexpected(
                 std::string("Invalid signature on account ") + toBase58(accountID) +
-                errorWhat.value_or("") + ".");
+                (errorWhat ? ": " + *errorWhat : "") + ".");
         }
     }
     // All signatures verified.
@@ -514,14 +520,18 @@ multiSignHelper(
 std::expected
 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
     // pre-constructing part of the data that we hash.  Fill a Serializer
     // with the stuff that stays constant from signature to signature.
+    auto const batchSignerAccount = batchSigner.getAccountID(sfAccount);
     Serializer dataStart;
-    serializeBatch(dataStart, getFlags(), getBatchTransactionIDs());
+    serializeBatch(
+        dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());
+    dataStart.addBitString(batchSignerAccount);
     return multiSignHelper(
         batchSigner,
-        std::nullopt,
+        batchSignerAccount,
         [&dataStart](AccountID const& accountID) -> Serializer {
             Serializer s = dataStart;
             finishMultiSigningData(accountID, s);
@@ -555,42 +565,38 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
         rules);
 }
 
-/**
- * @brief Retrieves a batch of transaction IDs from the STTx.
- *
- * This function returns a vector of transaction IDs by extracting them from
- * the field array `sfRawTransactions` within the STTx. If the batch
- * transaction IDs have already been computed and cached in `batchTxnIds_`,
- * it returns the cached vector. Otherwise, it computes the transaction IDs,
- * caches them, and then returns the vector.
- *
- * @return A vector of `uint256` containing the batch transaction IDs.
- *
- * @note The function asserts that the `sfRawTransactions` field array is not
- * empty and that the size of the computed batch transaction IDs matches the
- * size of the `sfRawTransactions` field array.
- */
+void
+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::buildBatchTxnIds : template applied");
+    if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions))
+        return;
+
+    auto const& raw = getFieldArray(sfRawTransactions);
+
+    // 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)
+        ids.push_back(rb.getHash(HashPrefix::TransactionId));
+}
+
 std::vector const&
 STTx::getBatchTransactionIDs() const
 {
-    XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : not a batch transaction");
+    XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction");
     XRPL_ASSERT(
-        !getFieldArray(sfRawTransactions).empty(),
-        "STTx::getBatchTransactionIDs : empty raw transactions");
-
-    // The list of inner ids is built once, then reused on subsequent calls.
-    // After the list is built, it must always have the same size as the array
-    // `sfRawTransactions`. The assert below verifies that.
-    if (batchTxnIds_.empty())
-    {
-        for (STObject const& rb : getFieldArray(sfRawTransactions))
-            batchTxnIds_.push_back(rb.getHash(HashPrefix::TransactionId));
-    }
-
+        batchTxnIds_.has_value(), "STTx::getBatchTransactionIDs : batch transaction IDs built");
     XRPL_ASSERT(
-        batchTxnIds_.size() == getFieldArray(sfRawTransactions).size(),
+        batchTxnIds_->size() == getFieldArray(sfRawTransactions).size(),
         "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch");
-    return batchTxnIds_;
+    return *batchTxnIds_;
 }
 
 //------------------------------------------------------------------------------
@@ -727,13 +733,22 @@ invalidMPTAmountInTx(STObject const& tx)
 }
 
 static bool
-isRawTransactionOkay(STObject const& st, std::string& reason)
+isBatchRawTransactionOkay(STObject const& st, std::string& reason)
 {
     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 (st.getFieldU16(sfTransactionType) != ttBATCH)
+    {
+        reason = "Only Batch transactions may contain raw transactions.";
+        return false;
+    }
+
     if (st.isFieldPresent(sfBatchSigners) &&
-        st.getFieldArray(sfBatchSigners).size() > kMaxBatchTxCount)
+        st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
     {
         reason = "Batch Signers array exceeds max entries.";
         return false;
@@ -757,6 +772,12 @@ isRawTransactionOkay(STObject const& st, std::string& reason)
             }
 
             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)
         {
@@ -791,7 +812,7 @@ passesLocalChecks(STObject const& st, std::string& reason)
         return false;
     }
 
-    if (!isRawTransactionOkay(st, reason))
+    if (!isBatchRawTransactionOkay(st, reason))
         return false;
 
     return true;
diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp
index e5c1d17b1a..a6f8192a2f 100644
--- a/src/libxrpl/protocol/TER.cpp
+++ b/src/libxrpl/protocol/TER.cpp
@@ -130,6 +130,8 @@ transResults()
         MAKE_ERROR(tefNO_TICKET,                   "Ticket is not in ledger."),
         MAKE_ERROR(tefNFTOKEN_IS_NOT_TRANSFERABLE, "The specified NFToken is not transferable."),
         MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE,     "The LedgerFixType field has an invalid value."),
+        MAKE_ERROR(tefNO_DST_PARTIAL,              "Partial payment to create account not allowed."),
+        MAKE_ERROR(tefBAD_PATH_COUNT,              "Malformed: Too many paths."),
 
         MAKE_ERROR(telLOCAL_ERROR,            "Local failure."),
         MAKE_ERROR(telBAD_DOMAIN,             "Domain too long."),
diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp
index 9ab8779f03..0f6988543e 100644
--- a/src/libxrpl/tx/Transactor.cpp
+++ b/src/libxrpl/tx/Transactor.cpp
@@ -1,6 +1,5 @@
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -221,13 +220,15 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
     if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
         return temINVALID;
 
-    if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch))
+    if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
         return temINVALID_FLAG;
 
-    XRPL_ASSERT(
-        ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() ||
-            !ctx.rules.enabled(featureBatch),
-        "Inner batch transaction must have a parent batch ID.");
+    // Reject if the inner batch flag and parentBatchId are inconsistent.
+    // A standalone tx with tfInnerBatchTxn but no parentBatchId is an
+    // attack attempt. A tx with parentBatchId but without tfInnerBatchTxn
+    // is a programming error.
+    if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value())
+        return temINVALID_INNER_BATCH;
 
     return tesSUCCESS;
 }
@@ -243,15 +244,19 @@ Transactor::preflight2(PreflightContext const& ctx)
         return *ret;
     }
 
-    // It should be impossible for the InnerBatchTxn flag to be set without
-    // featureBatch being enabled
-    XRPL_ASSERT_PARTS(
-        !ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch),
-        "xrpl::Transactor::preflight2",
-        "InnerBatch flag only set if feature enabled");
-    // Skip signature check on batch inner transactions
-    if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch))
+    // Skip the signature check on batch inner transactions. preflight1 already
+    // enforces both conditions; re-checking them as defense in depth guarantees
+    // we never return success (and so skip signature validation) for an inner
+    // transaction unless the amendment is enabled and it really sits inside a
+    // batch.
+    if (ctx.tx.isFlag(tfInnerBatchTxn))
+    {
+        if (!ctx.rules.enabled(featureBatchV1_1))
+            return temINVALID_FLAG;
+        if (!ctx.parentBatchId.has_value())
+            return temINVALID_INNER_BATCH;
         return tesSUCCESS;
+    }
     // Do not add any checks after this point that are relevant for
     // batch inner transactions. They will be skipped.
 
@@ -708,23 +713,26 @@ Transactor::checkSign(
     std::optional const& parentBatchId,
     AccountID const& idAccount,
     STObject const& sigObject,
-    beast::Journal const j)
+    beast::Journal const j,
+    bool permitUncreatedAccount)
 {
     {
         auto const sle = view.read(keylet::account(idAccount));
 
-        if (view.rules().enabled(featureLendingProtocol) && isPseudoAccount(sle))
+        if ((view.rules().enabled(featureLendingProtocol) ||
+             view.rules().enabled(featureBatchV1_1) || view.rules().enabled(fixCleanup3_3_0)) &&
+            isPseudoAccount(sle))
         {
-            // Pseudo-accounts can't sign transactions. This check is gated on
-            // the Lending Protocol amendment because that's the project it was
-            // added under, and it doesn't justify another amendment
+            // Pseudo-accounts can't sign transactions. This check is gated on a
+            // few different amendments so that it takes effect as soon as any of
+            // them is activated.
             return tefBAD_AUTH;
         }
     }
 
     auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
     // Ignore signature check on batch inner transactions
-    if (parentBatchId && view.rules().enabled(featureBatch))
+    if (parentBatchId && view.rules().enabled(featureBatchV1_1))
     {
         // Defensive Check: These values are also checked in Batch::preflight
         if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
@@ -762,7 +770,16 @@ Transactor::checkSign(
     auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
     auto const sleAccount = view.read(keylet::account(idAccount));
     if (!sleAccount)
-        return terNO_ACCOUNT;
+    {
+        // An account that does not exist yet can only be authorized by its own
+        // master key, and only where an un-created signer is permitted (a batch
+        // whose earlier inner creates the account). Otherwise it cannot sign.
+        if (!permitUncreatedAccount)
+            return terNO_ACCOUNT;
+        if (idAccount != idSigner)
+            return tefBAD_AUTH;
+        return tesSUCCESS;
+    }
 
     return checkSingleSign(view, idSigner, idAccount, sleAccount, j);
 }
@@ -775,50 +792,6 @@ Transactor::checkSign(PreclaimContext const& ctx)
     return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
 }
 
-NotTEC
-Transactor::checkBatchSign(PreclaimContext const& ctx)
-{
-    NotTEC ret = tesSUCCESS;
-    STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
-    for (auto const& signer : signers)
-    {
-        auto const idAccount = signer.getAccountID(sfAccount);
-
-        Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
-        if (pkSigner.empty())
-        {
-            if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
-                !isTesSuccess(ret))
-                return ret;
-        }
-        else
-        {
-            // LCOV_EXCL_START
-            if (!publicKeyType(makeSlice(pkSigner)))
-                return tefBAD_AUTH;
-            // LCOV_EXCL_STOP
-
-            auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
-            auto const sleAccount = ctx.view.read(keylet::account(idAccount));
-
-            // A batch can include transactions from an un-created account ONLY
-            // when the account master key is the signer
-            if (!sleAccount)
-            {
-                if (idAccount != idSigner)
-                    return tefBAD_AUTH;
-
-                return tesSUCCESS;
-            }
-
-            if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
-                !isTesSuccess(ret))
-                return ret;
-        }
-    }
-    return ret;
-}
-
 NotTEC
 Transactor::checkSingleSign(
     ReadView const& view,
@@ -1114,7 +1087,7 @@ Transactor::reset(XRPAmount fee)
 
     // balance should have already been checked in checkFee / preFlight.
     XRPL_ASSERT(
-        balance != beast::kZero && (!view().open() || balance >= fee),
+        (fee == beast::kZero || balance != beast::kZero) && (!view().open() || balance >= fee),
         "xrpl::Transactor::reset : valid balance");
 
     // We retry/reject the transaction if the account balance is zero or
diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp
index b70cb0d345..d85c4cfc40 100644
--- a/src/libxrpl/tx/apply.cpp
+++ b/src/libxrpl/tx/apply.cpp
@@ -8,7 +8,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -39,30 +38,14 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
     auto const id = tx.getTransactionID();
     auto const flags = router.getFlags(id);
 
-    // Ignore signature check on batch inner transactions
-    if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch))
+    // Batch inner transactions are never independently valid: they are applied
+    // within their batch, not through checkValidity. Reaching here means one was
+    // relayed or submitted on its own, so mark it bad regardless of the
+    // amendment (like PeerImp and NetworkOPs).
+    if (tx.isFlag(tfInnerBatchTxn))
     {
-        // Defensive Check: These values are also checked in Batch::preflight
-        if (tx.isFieldPresent(sfTxnSignature) || !tx.getSigningPubKey().empty() ||
-            tx.isFieldPresent(sfSigners))
-            return {Validity::SigBad, "Malformed: Invalid inner batch transaction."};
-
-        // This block should probably have never been included in the
-        // original `Batch` implementation. An inner transaction never
-        // has a valid signature.
-        bool const neverValid = rules.enabled(fixBatchInnerSigs);
-        if (!neverValid)
-        {
-            std::string reason;
-            if (!passesLocalChecks(tx, reason))
-            {
-                router.setFlags(id, kSfLocalbad);
-                return {Validity::SigGoodOnly, reason};
-            }
-
-            router.setFlags(id, kSfSiggood);
-            return {Validity::Valid, ""};
-        }
+        router.setFlags(id, kSfSigbad);
+        return {Validity::SigBad, "Batch inner transactions are never considered validly signed."};
     }
 
     if (any(flags & kSfSigbad))
@@ -183,6 +166,9 @@ applyBatchTransactions(
 
         // If the transaction should be applied push its changes to the
         // whole-batch view.
+        // NOTE: each inner tx is individually capped at kOversizeMetaDataCap;
+        // there is no aggregate cap here. Bounded by kMaxBatchTxCount * cap,
+        // which standalone txns can already produce in one ledger.
         if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter)))
             perTxBatchView.apply(batchView);
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp
index 903707320b..1ac387d1b1 100644
--- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp
@@ -57,7 +57,7 @@ LoanSet::preflight(PreflightContext const& ctx)
     auto const& tx = ctx.tx;
 
     // Special case for Batch inner transactions
-    if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) &&
+    if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) &&
         !tx.isFieldPresent(sfCounterparty))
     {
         auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0});
diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp
index 7f1e4d8079..6e3883572a 100644
--- a/src/libxrpl/tx/transactors/payment/Payment.cpp
+++ b/src/libxrpl/tx/transactors/payment/Payment.cpp
@@ -363,16 +363,21 @@ Payment::preclaim(PreclaimContext const& ctx)
             // transaction would succeed.
             return tecNO_DST;
         }
-        if (ctx.view.open() && partialPaymentAllowed)
+        // A partial payment may not fund a new account.
+        if (partialPaymentAllowed)
         {
-            // You cannot fund an account with a partial payment.
-            // Make retry work smaller, by rejecting this.
-            JLOG(ctx.j.trace()) << "Delay transaction: Partial payment not "
-                                   "allowed to create account.";
-
-            // Another transaction could create the account and then this
-            // transaction would succeed.
-            return telNO_DST_PARTIAL;
+            // Open view: the soft tel (unchanged).
+            if (ctx.view.open())
+            {
+                // Make retry work smaller, by rejecting this.
+                JLOG(ctx.j.trace()) << "Delay transaction: Partial payment not "
+                                       "allowed to create account.";
+                return telNO_DST_PARTIAL;
+            }
+            // Inner batch txns are claimed on a closed view, where a tel is
+            // invalid, so use the tef.
+            if (ctx.parentBatchId && ctx.view.rules().enabled(featureBatchV1_1))
+                return tefNO_DST_PARTIAL;
         }
         if (dstAmount < STAmount(ctx.view.fees().reserve))
         {
@@ -400,7 +405,7 @@ Payment::preclaim(PreclaimContext const& ctx)
     }
 
     // Payment with at least one intermediate step and uses transitive balances.
-    if ((hasPaths || sendMax || !dstAmount.native()) && ctx.view.open())
+    if (hasPaths || sendMax || !dstAmount.native())
     {
         STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths);
 
@@ -408,7 +413,12 @@ Payment::preclaim(PreclaimContext const& ctx)
                 return path.size() > kMaxPathLength;
             }))
         {
-            return telBAD_PATH_COUNT;
+            // Open view: the soft tel (unchanged). Inner batch txns are claimed
+            // on a closed view, where a tel is invalid, so use the tef.
+            if (ctx.view.open())
+                return telBAD_PATH_COUNT;
+            if (ctx.parentBatchId && ctx.view.rules().enabled(featureBatchV1_1))
+                return tefBAD_PATH_COUNT;
         }
     }
 
diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp
index 64a62ac273..16d27f98d9 100644
--- a/src/libxrpl/tx/transactors/system/Batch.cpp
+++ b/src/libxrpl/tx/transactors/system/Batch.cpp
@@ -3,6 +3,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -21,11 +23,13 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
@@ -107,13 +111,13 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
     }
 
     // Calculate the Signers/BatchSigners Fees
-    std::int32_t signerCount = 0;
+    std::uint32_t signerCount = 0;
     if (tx.isFieldPresent(sfBatchSigners))
     {
         auto const& signers = tx.getFieldArray(sfBatchSigners);
 
         // LCOV_EXCL_START
-        if (signers.size() > kMaxBatchTxCount)
+        if (signers.size() > kMaxBatchSigners)
         {
             JLOG(debugLog().error()) << "BatchTrace: Batch Signers array exceeds max entries.";
             return XRPAmount{kInitialXrp};
@@ -128,7 +132,16 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
             }
             else if (signer.isFieldPresent(sfSigners))
             {
-                signerCount += signer.getFieldArray(sfSigners).size();
+                auto const& nestedSigners = signer.getFieldArray(sfSigners);
+                // LCOV_EXCL_START
+                if (nestedSigners.size() > STTx::kMaxMultiSigners)
+                {
+                    JLOG(debugLog().error())
+                        << "BatchTrace: Nested Signers array exceeds max entries.";
+                    return kInitialXrp;
+                }
+                // LCOV_EXCL_STOP
+                signerCount += nestedSigners.size();
             }
         }
     }
@@ -149,7 +162,8 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
         JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerFees calculation.";
         return XRPAmount{kInitialXrp};
     }
-    if (txnFees + signerFees > maxAmount - batchBase)
+    XRPAmount const innerFees = txnFees + signerFees;
+    if (innerFees > maxAmount - batchBase)
     {
         JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in total fee calculation.";
         return XRPAmount{kInitialXrp};
@@ -157,7 +171,7 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
     // LCOV_EXCL_STOP
 
     // 10 drops per batch signature + sum of inner tx fees + batchBase
-    return signerFees + txnFees + batchBase;
+    return innerFees + batchBase;
 }
 
 std::uint32_t
@@ -227,6 +241,14 @@ Batch::preflight(PreflightContext const& ctx)
         return temARRAY_TOO_LARGE;
     }
 
+    if (ctx.tx.isFieldPresent(sfBatchSigners) &&
+        ctx.tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners)
+    {
+        JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
+                            << "signers array exceeds " << kMaxBatchSigners << " entries.";
+        return temARRAY_TOO_LARGE;
+    }
+
     // Validation Inner Batch Txns
     std::unordered_set uniqueHashes;
     std::unordered_map> accountSeqTicket;
@@ -384,46 +406,48 @@ Batch::preflight(PreflightContext const& ctx)
 NotTEC
 Batch::preflightSigValidated(PreflightContext const& ctx)
 {
+    XRPL_ASSERT(
+        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);
 
-    // Build the signers list
-    std::unordered_set requiredSigners;
+    // 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 requiredSigners;
+    requiredSigners.reserve(kMaxBatchSigners);
     for (STObject const& rb : rawTxns)
     {
-        auto const innerAccount = rb.getAccountID(sfAccount);
+        // 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.getFeePayer();
 
-        // If the inner account is the same as the outer account, do not add the
-        // inner account to the required signers set.
-        if (innerAccount != outerAccount)
-            requiredSigners.insert(innerAccount);
+        // The outer account signs the batch itself, so it is never added to the
+        // required signers.
+        if (authorizer != outerAccount)
+            requiredSigners.push_back(authorizer);
         // Some transactions have a Counterparty, who must also sign the
         // transaction if they are not the outer account
-        if (auto const counterparty = rb.at(~sfCounterparty);
+        if (auto const counterparty = rb[~sfCounterparty];
             counterparty && counterparty != outerAccount)
-            requiredSigners.insert(*counterparty);
+            requiredSigners.push_back(*counterparty);
     }
+    std::ranges::sort(requiredSigners);
+    auto const dupes = std::ranges::unique(requiredSigners);
+    requiredSigners.erase(dupes.begin(), dupes.end());
+
+    std::size_t numReqSignersMatched = 0;
 
     // Validation Batch Signers
-    std::unordered_set batchSigners;
     if (ctx.tx.isFieldPresent(sfBatchSigners))
     {
         STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners);
 
-        // Check that the batch signers array is not too large.
-        if (signers.size() > kMaxBatchTxCount)
-        {
-            JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
-                                << "signers array exceeds 8 entries.";
-            return temARRAY_TOO_LARGE;
-        }
-
-        // Add batch signers to the set to ensure all signer accounts are
-        // unique. Meanwhile, remove signer accounts from the set of inner
-        // transaction accounts (`requiredSigners`). By the end of the loop,
-        // `requiredSigners` should be empty, indicating that all inner
-        // accounts are matched with signers.
+        // Batch signers must be strictly ascending and match the required
+        // signers exactly; since both are sorted, each must be the next
+        // required signer.
+        AccountID lastBatchSigner{beast::kZero};
         for (auto const& signer : signers)
         {
             AccountID const signerAccount = signer.getAccountID(sfAccount);
@@ -434,40 +458,66 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
                 return temBAD_SIGNER;
             }
 
-            if (!batchSigners.insert(signerAccount).second)
+            if (lastBatchSigner == signerAccount)
             {
                 JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
                                     << "duplicate signer found: " << signerAccount;
-                return temREDUNDANT;
-            }
-
-            // Check that the batch signer is in the required signers set.
-            // Remove it if it does, as it can be crossed off the list.
-            if (requiredSigners.erase(signerAccount) == 0)
-            {
-                JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
-                                    << "no account signature for inner txn.";
                 return temBAD_SIGNER;
             }
-        }
 
-        // Check the batch signers signatures.
-        auto const sigResult = ctx.tx.checkBatchSign(ctx.rules);
+            if (lastBatchSigner > signerAccount)
+            {
+                JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
+                                    << "unsorted signers array: " << signerAccount;
+                return temBAD_SIGNER;
+            }
+            lastBatchSigner = signerAccount;
 
-        if (!sigResult)
-        {
-            JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
-                                << "invalid batch txn signature: " << sigResult.error();
-            return temBAD_SIGNATURE;
+            if (numReqSignersMatched >= requiredSigners.size() ||
+                requiredSigners[numReqSignersMatched] != signerAccount)
+            {
+                JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
+                                    << "missing signer or extra signer provided: " << signerAccount;
+                return temBAD_SIGNER;
+            }
+            ++numReqSignersMatched;
         }
     }
 
-    if (!requiredSigners.empty())
+    // Every required signer must be matched. Also covers sfBatchSigners being
+    // absent while inner txns require signers (numReqSignersMatched stays 0).
+    if (numReqSignersMatched != requiredSigners.size())
     {
         JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
                             << "invalid batch signers.";
         return temBAD_SIGNER;
     }
+
+    return tesSUCCESS;
+}
+
+NotTEC
+Batch::checkBatchSign(PreclaimContext const& ctx)
+{
+    STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
+    for (auto const& signer : signers)
+    {
+        // Reuse the standard signer-authorization rules so the batch and
+        // non-batch paths cannot drift. permitUncreatedAccount allows an inner
+        // from an account that an earlier inner creates, which must be
+        // authorized by its own master key.
+        auto const idAccount = signer.getAccountID(sfAccount);
+        if (auto const ret = Transactor::checkSign(
+                ctx.view,
+                ctx.flags,
+                ctx.parentBatchId,
+                idAccount,
+                signer,
+                ctx.j,
+                /*permitUncreatedAccount=*/true);
+            !isTesSuccess(ret))
+            return ret;
+    }
     return tesSUCCESS;
 }
 
@@ -479,7 +529,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
  * corresponding error code.
  *
  * Next, it verifies the batch-specific signature requirements by calling
- * Transactor::checkBatchSign. If this check fails, it also returns the
+ * Batch::checkBatchSign. If this check fails, it also returns the
  * corresponding error code.
  *
  * If both checks succeed, the function returns tesSUCCESS.
@@ -494,8 +544,11 @@ Batch::checkSign(PreclaimContext const& ctx)
     if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
         return ret;
 
-    if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret))
-        return ret;
+    if (ctx.tx.isFieldPresent(sfBatchSigners))
+    {
+        if (auto ret = checkBatchSign(ctx); !isTesSuccess(ret))
+            return ret;
+    }
 
     return tesSUCCESS;
 }
diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp
index 6aaae62155..f9c156fe06 100644
--- a/src/test/app/Batch_test.cpp
+++ b/src/test/app/Batch_test.cpp
@@ -1,5 +1,6 @@
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -47,12 +48,14 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -61,15 +64,17 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -200,16 +205,13 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        bool const withInnerSigFix = features[fixBatchInnerSigs];
-
         for (bool const withBatch : {true, false})
         {
-            testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled")
-                     << ", Inner Sig Fix: " << (withInnerSigFix ? "enabled" : "disabled");
+            testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled");
 
-            auto const amend = withBatch ? features : features - featureBatch;
+            auto const amend = withBatch ? features : features - featureBatchV1_1;
 
-            test::jtx::Env env{*this, amend};
+            Env env{*this, amend};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -230,12 +232,10 @@ class Batch_test : public beast::unit_test::Suite
             }
 
             // tfInnerBatchTxn
-            // If the feature is disabled, the transaction fails with
-            // temINVALID_FLAG. If the feature is enabled, the transaction fails
-            // early in checkValidity()
+            // A standalone transaction carrying this flag is never valid, so it
+            // is rejected early in checkValidity() regardless of the amendment.
             {
-                auto const txResult = withBatch ? Ter(telENV_RPC_FAILED) : Ter(temINVALID_FLAG);
-                env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), txResult);
+                env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
                 env.close();
             }
 
@@ -254,7 +254,7 @@ class Batch_test : public beast::unit_test::Suite
         //----------------------------------------------------------------------
         // preflight
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -431,6 +431,41 @@ class Batch_test : public beast::unit_test::Suite
             env.close();
         }
 
+        // temINVALID_INNER_BATCH: tfInnerBatchTxn set but no parentBatchId.
+        {
+            auto jtx = env.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
+            PreflightContext const pfCtx(
+                env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
+            auto const pf = Transactor::invokePreflight(pfCtx);
+            BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
+        }
+
+        // temINVALID_FLAG: tfInnerBatchTxn set but featureBatchV1_1 disabled.
+        {
+            Env disabledEnv{*this, features - featureBatchV1_1};
+            disabledEnv.fund(XRP(10000), alice, bob);
+            disabledEnv.close();
+            auto jtx = disabledEnv.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn));
+            PreflightContext const pfCtx(
+                disabledEnv.app(),
+                *jtx.stx,
+                disabledEnv.current()->rules(),
+                TapNone,
+                disabledEnv.journal);
+            auto const pf = Transactor::invokePreflight(pfCtx);
+            BEAST_EXPECT(pf == temINVALID_FLAG);
+        }
+
+        // temINVALID_INNER_BATCH: parentBatchId set but tfInnerBatchTxn not
+        // set.
+        {
+            auto jtx = env.jt(pay(alice, bob, XRP(1)));
+            PreflightContext const pfCtx(
+                env.app(), *jtx.stx, uint256{1}, env.current()->rules(), TapBatch, env.journal);
+            auto const pf = Transactor::invokePreflight(pfCtx);
+            BEAST_EXPECT(pf == temINVALID_INNER_BATCH);
+        }
+
         // temBAD_FEE: Batch: inner txn must have a fee of 0.
         {
             auto const seq = env.seq(alice);
@@ -537,16 +572,16 @@ class Batch_test : public beast::unit_test::Suite
             env.close();
         }
 
-        // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8
-        // entries.
+        // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds
+        // kMaxBatchSigners entries.
         // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay()
         {
             auto const seq = env.seq(alice);
-            auto const batchFee = batch::calcBatchFee(env, 9, 2);
+            auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
             env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
                 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
                 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
-                batch::Sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice),
+                batch::Sig(std::vector(kMaxBatchSigners + 1, bob)),
                 Ter(telENV_RPC_FAILED));
             env.close();
         }
@@ -563,7 +598,7 @@ class Batch_test : public beast::unit_test::Suite
             env.close();
         }
 
-        // temREDUNDANT: Batch: duplicate signer found
+        // temBAD_SIGNER: Batch: duplicate signer (caught by ascending order check)
         {
             auto const seq = env.seq(alice);
             auto const batchFee = batch::calcBatchFee(env, 2, 2);
@@ -571,7 +606,7 @@ class Batch_test : public beast::unit_test::Suite
                 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
                 batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
                 batch::Sig(bob, bob),
-                Ter(temREDUNDANT));
+                Ter(temBAD_SIGNER));
             env.close();
         }
 
@@ -611,7 +646,13 @@ class Batch_test : public beast::unit_test::Suite
                 batch::Inner(pay(bob, alice, XRP(5)), bobSeq));
 
             Serializer msg;
-            serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs());
+            serializeBatch(
+                msg,
+                jt.stx->getAccountID(sfAccount),
+                jt.stx->getSeqValue(),
+                tfAllOrNothing,
+                jt.stx->getBatchTransactionIDs());
+            finishMultiSigningData(bob.id(), msg);
             auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
             jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
                 bob.human();
@@ -620,7 +661,7 @@ class Batch_test : public beast::unit_test::Suite
             jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
                 strHex(Slice{sig.data(), sig.size()});
 
-            env(jt.jv, Ter(temBAD_SIGNATURE));
+            env(jt.jv, Ter(telENV_RPC_FAILED));
             env.close();
         }
 
@@ -649,7 +690,7 @@ class Batch_test : public beast::unit_test::Suite
         //----------------------------------------------------------------------
         // preclaim
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -890,7 +931,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -971,6 +1012,46 @@ class Batch_test : public beast::unit_test::Suite
             env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED));
             env.close();
         }
+
+        // Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2.
+        {
+            MPTIssue const issue(makeMptID(1, alice));
+            STAmount const mptAmt{issue, UINT64_C(100)};
+
+            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const seq = env.seq(alice);
+
+            json::Value tx1;
+            tx1[jss::TransactionType] = jss::OfferCreate;
+            tx1[jss::Account] = alice.human();
+            tx1[jss::TakerPays] = mptAmt.getJson(JsonOptions::Values::None);
+            tx1[jss::TakerGets] = XRP(10).value().getJson(JsonOptions::Values::None);
+
+            env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                batch::Inner(tx1, seq + 1),
+                batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
+                Ter(tesSUCCESS));
+            env.close();
+        }
+
+        // Invalid: inner txn with invalid memo (non-URL-safe MemoType)
+        {
+            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const seq = env.seq(alice);
+
+            auto tx1 = pay(alice, bob, XRP(10));
+            auto& ma = tx1["Memos"];
+            auto& mi = ma[ma.size()];
+            auto& m = mi["Memo"];
+            m["MemoType"] = strHex(std::string("\x01\x02\x03", 3));
+            m["MemoData"] = strHex(std::string("test"));
+
+            env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                batch::Inner(tx1, seq + 1),
+                batch::Inner(pay(alice, bob, XRP(1)), seq + 2),
+                Ter(telENV_RPC_FAILED));
+            env.close();
+        }
     }
 
     void
@@ -981,7 +1062,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -1239,7 +1320,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Bad Fee Without Signer
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1261,7 +1342,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Bad Fee With MultiSign
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1288,7 +1369,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Bad Fee With MultiSign + BatchSigners
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1317,7 +1398,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Bad Fee With MultiSign + BatchSigners.Signers
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1349,7 +1430,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Bad Fee With BatchSigners
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1373,7 +1454,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Bad Fee Dynamic Fee Calculation
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1412,7 +1493,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries.
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1437,7 +1518,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries.
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -1465,41 +1546,52 @@ class Batch_test : public beast::unit_test::Suite
             });
         }
 
-        // telENV_RPC_FAILED: Batch: signers array exceeds 8 entries.
+        // Regression: the relay-boundary local check (isBatchRawTransactionOkay)
+        // caps the signers array at kMaxBatchSigners, matching Batch::preflight -
+        // not kMaxBatchTxCount. A batch with more than kMaxBatchTxCount but at
+        // most kMaxBatchSigners signers must get past local checks and reach
+        // signer validation. Before the cap was aligned it was wrongly rejected
+        // at the boundary (telENV_RPC_FAILED) before preflight ran; now it
+        // reaches preflightSigValidated and fails there as extra signers
+        // (temBAD_SIGNER) rather than being dropped pre-engine.
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
             env.fund(XRP(10000), alice, bob);
             env.close();
 
+            // Over kMaxBatchTxCount (8) but within kMaxBatchSigners (24).
+            std::size_t const signerCount = kMaxBatchTxCount + 1;
+
             auto const aliceSeq = env.seq(alice);
-            auto const batchFee = batch::calcBatchFee(env, 9, 2);
+            auto const batchFee = batch::calcBatchFee(env, signerCount, 2);
             env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
                 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
                 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
-                batch::Sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob),
-                Ter(telENV_RPC_FAILED));
+                batch::Sig(std::vector(signerCount, bob)),
+                Ter(temBAD_SIGNER));
             env.close();
         }
 
-        // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries.
+        // temARRAY_TOO_LARGE: Batch preflight: signers array exceeds
+        // kMaxBatchSigners entries.
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
             env.fund(XRP(10000), alice, bob);
             env.close();
 
-            auto const batchFee = batch::calcBatchFee(env, 0, 9);
+            auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2);
             auto const aliceSeq = env.seq(alice);
             auto jt = env.jtnofill(
                 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
                 batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
                 batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2),
-                batch::Sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob));
+                batch::Sig(std::vector(kMaxBatchSigners + 1, bob)));
 
             env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
                 auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j);
@@ -1517,7 +1609,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -1677,7 +1769,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -1966,7 +2058,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -2267,7 +2359,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -2538,22 +2630,16 @@ class Batch_test : public beast::unit_test::Suite
     void
     doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
     {
-        bool const withInnerSigFix = features[fixBatchInnerSigs];
+        std::string const testName =
+            std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": ";
 
-        std::string const testName = [&]() {
-            std::stringstream ss;
-            ss << "inner submit rpc: batch " << (withBatch ? "enabled" : "disabled")
-               << ", inner sig fix: " << (withInnerSigFix ? "enabled" : "disabled") << ": ";
-            return ss.str();
-        }();
-
-        auto const amend = withBatch ? features : features - featureBatch;
+        auto const amend = withBatch ? features : features - featureBatchV1_1;
 
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, amend};
-        if (!BEAST_EXPECT(amend[featureBatch] == withBatch))
+        Env env{*this, amend};
+        if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch))
             return;
 
         auto const alice = Account("alice");
@@ -2562,37 +2648,20 @@ class Batch_test : public beast::unit_test::Suite
         env.fund(XRP(10000), alice, bob);
         env.close();
 
-        auto submitAndValidate = [&](std::string caseName,
-                                     Slice const& slice,
-                                     int line,
-                                     std::optional expectedEnabled = std::nullopt,
-                                     std::optional expectedDisabled = std::nullopt,
-                                     bool expectInvalidFlag = false) {
-            testcase << testName << caseName
-                     << (expectInvalidFlag ? " - Expected to reach tx engine!" : "");
+        // Any transaction carrying tfInnerBatchTxn is rejected in checkValidity()
+        // before it reaches the tx engine, regardless of its signing fields or
+        // whether the amendment is enabled.
+        auto submitAndValidate = [&](std::string caseName, Slice const& slice, int line) {
+            testcase << testName << caseName;
             auto const jrr = env.rpc("submit", strHex(slice))[jss::result];
-            auto const expected = withBatch
-                ? expectedEnabled.value_or(
-                      "fails local checks: Malformed: Invalid inner batch "
-                      "transaction.")
-                : expectedDisabled.value_or("fails local checks: Empty SigningPubKey.");
-            if (expectInvalidFlag)
-            {
-                expect(
-                    jrr[jss::status] == "success" && jrr[jss::engine_result] == "temINVALID_FLAG",
-                    pretty(jrr),
-                    __FILE__,
-                    line);
-            }
-            else
-            {
-                expect(
-                    jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
-                        jrr[jss::error_exception] == expected,
-                    pretty(jrr),
-                    __FILE__,
-                    line);
-            }
+            expect(
+                jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" &&
+                    jrr[jss::error_exception] ==
+                        "fails local checks: Batch inner transactions are never "
+                        "considered validly signed.",
+                pretty(jrr),
+                __FILE__,
+                line);
             env.close();
         };
 
@@ -2621,12 +2690,7 @@ class Batch_test : public beast::unit_test::Suite
             STParsedJSONObject parsed("test", txn.getTxn());
             Serializer s;
             parsed.object->add(s);  // NOLINT(bugprone-unchecked-optional-access)
-            submitAndValidate(
-                "SigningPubKey set",
-                s.slice(),
-                __LINE__,
-                std::nullopt,
-                "fails local checks: Invalid signature.");
+            submitAndValidate("SigningPubKey set", s.slice(), __LINE__);
         }
 
         // Invalid RPC Submission: Signers
@@ -2640,12 +2704,7 @@ class Batch_test : public beast::unit_test::Suite
             STParsedJSONObject parsed("test", txn.getTxn());
             Serializer s;
             parsed.object->add(s);  // NOLINT(bugprone-unchecked-optional-access)
-            submitAndValidate(
-                "Signers set",
-                s.slice(),
-                __LINE__,
-                std::nullopt,
-                "fails local checks: Invalid Signers array size.");
+            submitAndValidate("Signers set", s.slice(), __LINE__);
         }
 
         {
@@ -2656,8 +2715,7 @@ class Batch_test : public beast::unit_test::Suite
             STParsedJSONObject parsed("test", jt.jv);
             Serializer s;
             parsed.object->add(s);  // NOLINT(bugprone-unchecked-optional-access)
-            submitAndValidate(
-                "Fully signed", s.slice(), __LINE__, std::nullopt, std::nullopt, !withBatch);
+            submitAndValidate("Fully signed", s.slice(), __LINE__);
         }
 
         // Invalid RPC Submission: tfInnerBatchTxn
@@ -2670,13 +2728,7 @@ class Batch_test : public beast::unit_test::Suite
             STParsedJSONObject parsed("test", txn.getTxn());
             Serializer s;
             parsed.object->add(s);  // NOLINT(bugprone-unchecked-optional-access)
-            submitAndValidate(
-                "No signing fields set",
-                s.slice(),
-                __LINE__,
-                "fails local checks: Empty SigningPubKey.",
-                "fails local checks: Empty SigningPubKey.",
-                withBatch && !withInnerSigFix);
+            submitAndValidate("No signing fields set", s.slice(), __LINE__);
         }
 
         // Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
@@ -2687,7 +2739,7 @@ class Batch_test : public beast::unit_test::Suite
         {
             STTx const amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
                 obj.setAccountID(sfAccount, AccountID());
-                obj.setFieldH256(sfAmendment, fixBatchInnerSigs);
+                obj.setFieldH256(sfAmendment, featureBatchV1_1);
                 obj.setFieldU32(sfLedgerSequence, seq);
                 obj.setFieldU32(sfFlags, tfInnerBatchTxn);
             });
@@ -2695,13 +2747,7 @@ class Batch_test : public beast::unit_test::Suite
             STParsedJSONObject parsed("test", txn.getTxn());
             Serializer s;
             parsed.object->add(s);  // NOLINT(bugprone-unchecked-optional-access)
-            submitAndValidate(
-                "Pseudo-transaction",
-                s.slice(),
-                __LINE__,
-                withInnerSigFix ? "fails local checks: Empty SigningPubKey."
-                                : "fails local checks: Cannot submit pseudo transactions.",
-                "fails local checks: Empty SigningPubKey.");
+            submitAndValidate("Pseudo-transaction", s.slice(), __LINE__);
         }
     }
 
@@ -2722,7 +2768,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -2771,6 +2817,93 @@ class Batch_test : public beast::unit_test::Suite
         // Alice pays XRP & Fee; Bob receives XRP
         BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee);
         BEAST_EXPECT(env.balance(bob) == XRP(1000));
+
+        // An inner partial payment must never fund a new account. With
+        // featureBatchV1_1 the partial-payment-to-create check runs on the
+        // batch's closed view and rejects it (tefNO_DST_PARTIAL). A USD->XRP path is
+        // set up so that, absent the check, the partial payment would deliver
+        // XRP and create carol; the check must block exactly that. A
+        // cross-currency shape (IOU SendMax, XRP Amount) also lets the inner
+        // pass preflight and reach the preclaim check.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const gw = Account("gw");
+            auto const carol = Account("carol");  // never funded
+            auto const usd = gw["USD"];
+            env.fund(XRP(10000), alice, gw);
+            env.close();
+            env.memoize(carol);
+
+            env(trust(alice, usd(100000)));
+            env(pay(gw, alice, usd(10000)));
+            env(offer(gw, usd(2000), XRP(2000)));  // USD -> XRP liquidity
+            env.close();
+
+            auto const seq = env.seq(alice);
+            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+
+            auto pp = pay(alice, carol, XRP(1000));
+            pp[jss::SendMax] = usd(2000).value().getJson(JsonOptions::Values::None);
+            pp[jss::Flags] = tfPartialPayment;
+
+            env(batch::outer(alice, seq, batchFee, tfIndependent),
+                batch::Inner(pp, seq + 1),
+                batch::Inner(noop(alice), seq + 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // carol was not created despite the available path.
+            BEAST_EXPECT(!env.le(carol));
+        }
+    }
+
+    void
+    testCheckAllSignatures(FeatureBitset features)
+    {
+        testcase("check all signatures");
+
+        using namespace test::jtx;
+        using namespace std::literals;
+
+        // Verifies that checkBatchSign validates all signers even when an
+        // unfunded account (signed with its master key) appears first in the
+        // sorted signer list. A funded account with an invalid signature must
+        // still be rejected with tefBAD_AUTH.
+
+        Env env{*this, features};
+
+        auto const alice = Account("alice");
+        // "aaa" sorts before other accounts alphabetically, ensuring the
+        // unfunded account is checked first in the sorted signer list
+        auto const unfunded = Account("aaa");
+        auto const carol = Account("carol");
+        env.fund(XRP(10000), alice, carol);
+        env.close();
+
+        // Verify sort order: unfunded.id() < carol.id()
+        BEAST_EXPECT(unfunded.id() < carol.id());
+
+        auto const seq = env.seq(alice);
+        auto const ledSeq = env.current()->seq();
+        auto const batchFee = batch::calcBatchFee(env, 2, 3);
+
+        // The batch includes:
+        // 1. alice pays unfunded (to create unfunded's account)
+        // 2. unfunded does a noop (signed by unfunded's master key - valid)
+        // 3. carol pays alice (signed by alice's key - INVALID since alice is
+        //    not carol's regular key)
+        //
+        // checkBatchSign must validate all signers regardless of order.
+        // This must fail with tefBAD_AUTH.
+        env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+            batch::Inner(pay(alice, unfunded, XRP(100)), seq + 1),
+            batch::Inner(noop(unfunded), ledSeq),
+            batch::Inner(pay(carol, alice, XRP(1000)), env.seq(carol)),
+            batch::Sig(unfunded, Reg{carol, alice}),
+            Ter(tefBAD_AUTH));
+        env.close();
     }
 
     void
@@ -2781,7 +2914,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -2845,7 +2978,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // tfIndependent: account delete success
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -2897,7 +3030,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // tfIndependent: account delete fails
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -2957,7 +3090,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // tfAllOrNothing: account delete fails
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -3009,7 +3142,7 @@ class Batch_test : public beast::unit_test::Suite
 
         using namespace test::jtx;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         Account const issuer{"issuer"};
         // For simplicity, lender will be the sole actor for the vault &
@@ -3186,7 +3319,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -3322,7 +3455,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -3396,7 +3529,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -3470,7 +3603,7 @@ class Batch_test : public beast::unit_test::Suite
             using namespace test::jtx;
             using namespace std::literals;
 
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -3531,7 +3664,7 @@ class Batch_test : public beast::unit_test::Suite
             using namespace test::jtx;
             using namespace std::literals;
 
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -3591,7 +3724,7 @@ class Batch_test : public beast::unit_test::Suite
             using namespace test::jtx;
             using namespace std::literals;
 
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -3666,7 +3799,7 @@ class Batch_test : public beast::unit_test::Suite
             // overwritten by the payment in the batch transaction. Because the
             // terPRE_SEQ is outside of the batch this noop transaction will ge
             // reapplied in the following ledger
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob, carol);
             env.close();
 
@@ -3729,7 +3862,7 @@ class Batch_test : public beast::unit_test::Suite
             // IMPORTANT: The batch txn is applied first, then the noop txn.
             // Because of this ordering, the noop txn is not applied and is
             // overwritten by the payment in the batch transaction.
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -3783,7 +3916,7 @@ class Batch_test : public beast::unit_test::Suite
             // IMPORTANT: The batch txn is applied first, then the noop txn.
             // Because of this ordering, the noop txn is not applied and is
             // overwritten by the payment in the batch transaction.
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -3831,7 +3964,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Outer Batch terPRE_SEQ
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob, carol);
             env.close();
 
@@ -3905,7 +4038,7 @@ class Batch_test : public beast::unit_test::Suite
             // IMPORTANT: The batch txn is applied first, then the noop txn.
             // Because of this ordering, the noop txn is not applied and is
             // overwritten by the payment in the batch transaction.
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -3964,7 +4097,7 @@ class Batch_test : public beast::unit_test::Suite
             // IMPORTANT: The batch txn is applied first, then the noop txn.
             // Because of this ordering, the noop txn is not applied and is
             // overwritten by the payment in the batch transaction.
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -4037,7 +4170,7 @@ class Batch_test : public beast::unit_test::Suite
             // batch will run in the close ledger process. The batch will be
             // allied and then retry this transaction in the current ledger.
 
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -4100,7 +4233,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // Create Object Before Batch Txn
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -4163,7 +4296,7 @@ class Batch_test : public beast::unit_test::Suite
             // batch will run in the close ledger process. The batch will be
             // applied and then retry this transaction in the current ledger.
 
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             env.fund(XRP(10000), alice, bob);
             env.close();
 
@@ -4226,7 +4359,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
 
         auto const alice = Account("alice");
         auto const bob = Account("bob");
@@ -4265,7 +4398,7 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        test::jtx::Env env{*this, features};
+        Env env{*this, features};
         XRPAmount const baseFee = env.current()->fees().base;
 
         auto const alice = Account("alice");
@@ -4359,9 +4492,12 @@ class Batch_test : public beast::unit_test::Suite
         using namespace test::jtx;
         using namespace std::literals;
 
-        // only outer batch transactions are counter towards the queue size
+        // A Batch is never queued. Under open-ledger congestion a Batch that
+        // pays only its base fee cannot apply directly and, unlike an ordinary
+        // transaction, is rejected outright rather than held in the queue. A
+        // Batch that pays the escalated open-ledger fee applies directly.
         {
-            test::jtx::Env env{
+            Env env{
                 *this,
                 makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
                 features,
@@ -4378,7 +4514,7 @@ class Batch_test : public beast::unit_test::Suite
             env.fund(XRP(10000), noripple(carol));
             env.close(env.now() + 5s, 10000ms);
 
-            // Fill the ledger
+            // Fill the open ledger so escalation is active.
             env(noop(alice));
             env(noop(alice));
             env(noop(alice));
@@ -4391,33 +4527,28 @@ class Batch_test : public beast::unit_test::Suite
             auto const bobSeq = env.seq(bob);
             auto const batchFee = batch::calcBatchFee(env, 1, 2);
 
-            // Queue Batch
-            {
-                env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
-                    batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
-                    batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
-                    batch::Sig(bob),
-                    Ter(terQUEUED));
-            }
+            // Paying only the base fee, the Batch cannot enter the congested
+            // open ledger and is rejected rather than queued.
+            env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
+                batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
+                batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
+                batch::Sig(bob),
+                Ter(telCAN_NOT_QUEUE));
 
-            checkMetrics(*this, env, 2, std::nullopt, 3, 2);
+            // The Batch was not queued; only carol's transaction is queued.
+            checkMetrics(*this, env, 1, std::nullopt, 3, 2);
 
-            // Replace Queued Batch
-            {
-                env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
-                    batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
-                    batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
-                    batch::Sig(bob),
-                    Ter(tesSUCCESS));
-                env.close();
-            }
-
-            checkMetrics(*this, env, 0, 12, 1, 6);
+            // Paying the escalated open-ledger fee, the Batch applies directly.
+            env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
+                batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
+                batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
+                batch::Sig(bob),
+                Ter(tesSUCCESS));
         }
 
         // inner batch transactions are counter towards the ledger tx count
         {
-            test::jtx::Env env{
+            Env env{
                 *this,
                 makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
                 features,
@@ -4457,6 +4588,50 @@ class Batch_test : public beast::unit_test::Suite
             env(noop(carol), Ter(terQUEUED));
             checkMetrics(*this, env, 1, std::nullopt, 3, 2);
         }
+
+        // A Batch is never queued, so it also cannot sit behind the account's
+        // already-queued transactions: with a sequence gap it cannot apply
+        // directly and is rejected rather than queued.
+        {
+            Env env{
+                *this,
+                makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
+                features,
+                nullptr,
+                beast::Severity::Error};
+
+            auto alice = Account("alice");
+            auto bob = Account("bob");
+
+            env.fund(XRP(10000), noripple(alice, bob));
+            env.close(env.now() + 5s, 10000ms);
+
+            // Fill the open ledger so subsequent transactions queue.
+            env(noop(alice));
+            env(noop(alice));
+            env(noop(alice));
+            checkMetrics(*this, env, 0, std::nullopt, 3, 2);
+
+            auto const aliceSeq = env.seq(alice);
+
+            // Queue two normal transactions for alice.
+            env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED));
+            env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED));
+            checkMetrics(*this, env, 2, std::nullopt, 3, 2);
+
+            // The Batch's sequence sits behind the queued transactions, so it
+            // cannot apply directly and is rejected rather than queued.
+            auto const bobSeq = env.seq(bob);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            env(batch::outer(alice, aliceSeq + 2, batchFee, tfAllOrNothing),
+                batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 3),
+                batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
+                batch::Sig(bob),
+                Ter(telCAN_NOT_QUEUE));
+
+            // The two queued transactions are untouched.
+            checkMetrics(*this, env, 2, std::nullopt, 3, 2);
+        }
     }
 
     void
@@ -4508,6 +4683,376 @@ class Batch_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testBatchDelegateConsent(FeatureBitset features)
+    {
+        testcase("batch delegate consent");
+
+        using namespace test::jtx;
+        using namespace std::literals;
+
+        // Delegate consent bypass.
+        //
+        // Alice delegates Payment to Bob. A Batch carries an inner Payment with
+        // Delegate=Bob, but Bob never signs the batch (and, because the inner
+        // Account == the outer Account, no BatchSigners are required at all).
+        // Batch waives the per-inner signature check, and preflightSigValidated
+        // derives required signers from sfAccount only -- it ignores sfDelegate
+        // -- so Bob's delegated authority is exercised without Bob's consent.
+        //
+        // SECURE EXPECTATION: the delegate (Bob) must authorize the inner txn,
+        // so the batch must be rejected (temBAD_SIGNER) when Bob does not sign.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const mallory = Account("mallory");
+            env.fund(XRP(10000), alice, bob, mallory);
+            env.close();
+
+            env(delegate::set(alice, bob, {"Payment"}));
+            env.close();
+
+            auto const preMallory = env.balance(mallory);
+
+            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const seq = env.seq(alice);
+
+            auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
+            inner[jss::Delegate] = bob.human();
+
+            // Bob is NOT among the batch signers; he never consents.
+            env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                inner,
+                batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
+                Ter(temBAD_SIGNER));
+            env.close();
+
+            // No funds should have moved to Mallory.
+            BEAST_EXPECT(env.balance(mallory) == preMallory);
+        }
+
+        // Self-grant attribution forgery.
+        //
+        // With no pre-existing delegation, Alice places DelegateSet(authorize
+        // Bob) and a Delegate=Bob action in the SAME batch. The second inner
+        // reads the delegation created by the first from the batch's running
+        // view, and Bob never signs -- manufacturing on-chain attribution of
+        // the action to Bob without his consent.
+        //
+        // SECURE EXPECTATION: rejected (temBAD_SIGNER) -- Bob must consent, and
+        // a grant cannot be created and exercised within the same atomic batch.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const mallory = Account("mallory");
+            env.fund(XRP(10000), alice, bob, mallory);
+            env.close();
+
+            auto const preMallory = env.balance(mallory);
+
+            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const seq = env.seq(alice);
+
+            auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
+            inner[jss::Delegate] = bob.human();
+
+            env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
+                inner,
+                Ter(temBAD_SIGNER));
+            env.close();
+
+            BEAST_EXPECT(env.balance(mallory) == preMallory);
+        }
+
+        // Legitimate counterpart: the same atomic grant-and-use is allowed when
+        // the delegate co-signs the batch -- consent is present, so it succeeds.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const mallory = Account("mallory");
+            env.fund(XRP(10000), alice, bob, mallory);
+            env.close();
+
+            auto const preMallory = env.balance(mallory);
+
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            auto const seq = env.seq(alice);
+
+            auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2);
+            inner[jss::Delegate] = bob.human();
+
+            env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1),
+                inner,
+                batch::Sig(bob),
+                Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
+        }
+
+        // Multi-account: a delegated inner from a non-outer account also
+        // requires the delegate's signature (not the account holder's).
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const carol = Account("carol");
+            env.fund(XRP(10000), alice, bob, carol);
+            env.close();
+
+            env(delegate::set(bob, carol, {"Payment"}));
+            env.close();
+
+            auto const preAlice = env.balance(alice);
+
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            auto const aliceSeq = env.seq(alice);
+            auto const bobSeq = env.seq(bob);
+
+            auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
+            inner[jss::Delegate] = carol.human();
+
+            // Carol (the delegate) does not sign.
+            env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
+                inner,
+                batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
+                Ter(temBAD_SIGNER));
+            env.close();
+
+            BEAST_EXPECT(env.balance(alice) == preAlice);
+        }
+
+        // Wrong signer: a batch signature from someone other than the named
+        // delegate does not satisfy the requirement.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const carol = Account("carol");
+            auto const mallory = Account("mallory");
+            env.fund(XRP(10000), alice, bob, carol, mallory);
+            env.close();
+
+            env(delegate::set(alice, bob, {"Payment"}));
+            env.close();
+
+            auto const preMallory = env.balance(mallory);
+
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            auto const seq = env.seq(alice);
+
+            auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1);
+            inner[jss::Delegate] = bob.human();
+
+            // Carol signs instead of the named delegate Bob.
+            env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                inner,
+                batch::Inner(pay(alice, mallory, XRP(1)), seq + 2),
+                batch::Sig(carol),
+                Ter(temBAD_SIGNER));
+            env.close();
+
+            BEAST_EXPECT(env.balance(mallory) == preMallory);
+        }
+
+        // Delegate is the outer account: the outer signature already provides
+        // the delegate's consent, so no BatchSigners are required.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            env.fund(XRP(10000), alice, bob);
+            env.close();
+
+            // Bob delegates Payment to Alice, who is also the batch submitter.
+            env(delegate::set(bob, alice, {"Payment"}));
+            env.close();
+
+            auto const preBob = env.balance(bob);
+
+            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const aliceSeq = env.seq(alice);
+            auto const bobSeq = env.seq(bob);
+
+            auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
+            inner[jss::Delegate] = alice.human();
+
+            env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
+                inner,
+                batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Net: Bob sends 1 to Alice, receives 2 from Alice.
+            BEAST_EXPECT(env.balance(bob) == preBob + XRP(1));
+        }
+
+        // Principal signs instead of the delegate. Bob delegates Payment to
+        // Carol and an inner pays from Bob with Delegate=Carol, so Carol -- not
+        // Bob -- is the required batch signer. Bob's own signature (the account
+        // holder) does not satisfy the delegate-consent requirement.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const carol = Account("carol");
+            env.fund(XRP(10000), alice, bob, carol);
+            env.close();
+
+            env(delegate::set(bob, carol, {"Payment"}));
+            env.close();
+
+            auto const preAlice = env.balance(alice);
+
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            auto const aliceSeq = env.seq(alice);
+            auto const bobSeq = env.seq(bob);
+
+            auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
+            inner[jss::Delegate] = carol.human();
+
+            // Bob (the principal) signs in place of Carol (the delegate).
+            env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
+                inner,
+                batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
+                batch::Sig(bob),
+                Ter(temBAD_SIGNER));
+            env.close();
+
+            BEAST_EXPECT(env.balance(alice) == preAlice);
+        }
+
+        // Control for the revocation case below: identical setup, but inner 1
+        // is a benign payment instead of a revocation. With Alice's delegation
+        // intact, the delegated inner 2 succeeds -- proving the failure in the
+        // revocation case is caused by the revocation, not the setup.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const mallory = Account("mallory");
+            env.fund(XRP(10000), alice, bob, mallory);
+            env.close();
+
+            env(delegate::set(bob, alice, {"Payment"}));
+            env.close();
+
+            auto const preMallory = env.balance(mallory);
+
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            auto const seq = env.seq(bob);
+
+            auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
+            inner2[jss::Delegate] = alice.human();
+
+            // Inner 1 is Bob's own payment (no revocation); Alice co-signs for
+            // the delegated inner 2.
+            auto const [txIDs, batchID] = submitBatch(
+                env,
+                tesSUCCESS,
+                batch::outer(bob, seq, batchFee, tfIndependent),
+                batch::Inner(pay(bob, alice, XRP(1)), seq + 1),
+                inner2,
+                batch::Sig(alice));
+            env.close();
+
+            std::vector const testCases = {
+                {.index = 0,
+                 .txType = "Batch",
+                 .result = "tesSUCCESS",
+                 .txHash = batchID,
+                 .batchID = std::nullopt},
+                {.index = 1,
+                 .txType = "Payment",
+                 .result = "tesSUCCESS",
+                 .txHash = txIDs[0],
+                 .batchID = batchID},
+                {.index = 2,
+                 .txType = "Payment",
+                 .result = "tesSUCCESS",
+                 .txHash = txIDs[1],
+                 .batchID = batchID},
+            };
+            validateClosedLedger(env, testCases);
+
+            // The delegated payment applied: Mallory received the funds.
+            BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000));
+        }
+
+        // Revocation within the same batch. Bob grants Alice Payment permission
+        // beforehand; an earlier inner revokes it before a later delegated
+        // inner tries to use it. Alice must still co-sign (delegate consent is
+        // derived from the static tx), but the revoked permission makes the
+        // delegated inner fail at apply time -- a grant cannot be used after it
+        // is removed earlier in the same batch.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const mallory = Account("mallory");
+            env.fund(XRP(10000), alice, bob, mallory);
+            env.close();
+
+            env(delegate::set(bob, alice, {"Payment"}));
+            env.close();
+
+            auto const preMallory = env.balance(mallory);
+
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            auto const seq = env.seq(bob);
+
+            // Inner 2: Alice acts as Bob's delegate, so Alice is the required
+            // batch signer.
+            auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2);
+            inner2[jss::Delegate] = alice.human();
+
+            // tfIndependent: inner 1 (the revocation) applies; inner 2 then
+            // fails for lack of permission without reverting inner 1. An empty
+            // permission list deletes the Delegate object.
+            auto const [txIDs, batchID] = submitBatch(
+                env,
+                tesSUCCESS,
+                batch::outer(bob, seq, batchFee, tfIndependent),
+                batch::Inner(delegate::set(bob, alice, {}), seq + 1),
+                inner2,
+                batch::Sig(alice));
+            env.close();
+
+            std::vector const testCases = {
+                {.index = 0,
+                 .txType = "Batch",
+                 .result = "tesSUCCESS",
+                 .txHash = batchID,
+                 .batchID = std::nullopt},
+                {.index = 1,
+                 .txType = "DelegateSet",
+                 .result = "tesSUCCESS",
+                 .txHash = txIDs[0],
+                 .batchID = batchID},
+                // inner 2 fails: Alice's permission was revoked in inner 1.
+            };
+            validateClosedLedger(env, testCases);
+
+            // The delegated payment never applied.
+            BEAST_EXPECT(env.balance(mallory) == preMallory);
+            BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound");
+        }
+    }
+
     void
     testBatchDelegate(FeatureBitset features)
     {
@@ -4518,7 +5063,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // delegated non atomic inner
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -4533,17 +5078,20 @@ class Batch_test : public beast::unit_test::Suite
             auto const preAlice = env.balance(alice);
             auto const preBob = env.balance(bob);
 
-            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
             auto const seq = env.seq(alice);
 
             auto tx = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
             tx[jss::Delegate] = bob.human();
+            // The delegate (Bob) authorizes the delegated inner, so Bob must
+            // provide the batch signature.
             auto const [txIDs, batchID] = submitBatch(
                 env,
                 tesSUCCESS,
                 batch::outer(alice, seq, batchFee, tfAllOrNothing),
                 tx,
-                batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
+                batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
+                batch::Sig(bob));
             env.close();
 
             std::vector const testCases = {
@@ -4575,7 +5123,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // delegated atomic inner
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -4598,13 +5146,15 @@ class Batch_test : public beast::unit_test::Suite
 
             auto tx = batch::Inner(pay(bob, alice, XRP(1)), bobSeq);
             tx[jss::Delegate] = carol.human();
+            // Carol is the delegate authorizing the inner txn on Bob's behalf,
+            // so Carol -- not Bob -- must provide the batch signature.
             auto const [txIDs, batchID] = submitBatch(
                 env,
                 tesSUCCESS,
                 batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
                 tx,
                 batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1),
-                batch::Sig(bob));
+                batch::Sig(carol));
             env.close();
 
             std::vector const testCases = {
@@ -4639,7 +5189,7 @@ class Batch_test : public beast::unit_test::Suite
         // this also makes sure tfInnerBatchTxn won't block delegated AccountSet
         // with granular permission
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
 
             auto const alice = Account("alice");
             auto const bob = Account("bob");
@@ -4654,19 +5204,22 @@ class Batch_test : public beast::unit_test::Suite
             auto const preAlice = env.balance(alice);
             auto const preBob = env.balance(bob);
 
-            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
             auto const seq = env.seq(alice);
 
             auto tx = batch::Inner(noop(alice), seq + 1);
             std::string const domain = "example.com";
             tx[sfDomain.jsonName] = strHex(domain);
             tx[jss::Delegate] = bob.human();
+            // Bob is the delegate authorizing the inner AccountSet, so Bob must
+            // provide the batch signature.
             auto const [txIDs, batchID] = submitBatch(
                 env,
                 tesSUCCESS,
                 batch::outer(alice, seq, batchFee, tfAllOrNothing),
                 tx,
-                batch::Inner(pay(alice, bob, XRP(2)), seq + 2));
+                batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
+                batch::Sig(bob));
             env.close();
 
             std::vector const testCases = {
@@ -4700,7 +5253,7 @@ class Batch_test : public beast::unit_test::Suite
         // this also makes sure tfInnerBatchTxn won't block delegated
         // MPTokenIssuanceSet with granular permission
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             Account const alice{"alice"};
             Account const bob{"bob"};
             env.fund(XRP(100000), alice, bob);
@@ -4717,7 +5270,7 @@ class Batch_test : public beast::unit_test::Suite
             env.close();
 
             auto const seq = env.seq(alice);
-            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
 
             json::Value jv1;
             jv1[sfTransactionType] = jss::MPTokenIssuanceSet;
@@ -4735,12 +5288,15 @@ class Batch_test : public beast::unit_test::Suite
             jv2[sfMPTokenIssuanceID] = to_string(mptID);
             jv2[sfFlags] = tfMPTUnlock;
 
+            // Both inners are delegated to Bob, so Bob must provide the batch
+            // signature (one signer covers both).
             auto const [txIDs, batchID] = submitBatch(
                 env,
                 tesSUCCESS,
                 batch::outer(alice, seq, batchFee, tfAllOrNothing),
                 batch::Inner(jv1, seq + 1),
-                batch::Inner(jv2, seq + 2));
+                batch::Inner(jv2, seq + 2),
+                batch::Sig(bob));
             env.close();
 
             std::vector const testCases = {
@@ -4767,7 +5323,7 @@ class Batch_test : public beast::unit_test::Suite
         // this also makes sure tfInnerBatchTxn won't block delegated TrustSet
         // with granular permission
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             Account const gw{"gw"};
             Account const alice{"alice"};
             Account const bob{"bob"};
@@ -4781,19 +5337,22 @@ class Batch_test : public beast::unit_test::Suite
             env.close();
 
             auto const seq = env.seq(gw);
-            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
 
             auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth);
             jv1[sfDelegate] = bob.human();
             auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
             jv2[sfDelegate] = bob.human();
 
+            // Both inners are delegated to Bob, so Bob must provide the batch
+            // signature.
             auto const [txIDs, batchID] = submitBatch(
                 env,
                 tesSUCCESS,
                 batch::outer(gw, seq, batchFee, tfAllOrNothing),
                 batch::Inner(jv1, seq + 1),
-                batch::Inner(jv2, seq + 2));
+                batch::Inner(jv2, seq + 2),
+                batch::Sig(bob));
             env.close();
 
             std::vector const testCases = {
@@ -4818,7 +5377,7 @@ class Batch_test : public beast::unit_test::Suite
 
         // inner transaction not authorized by the delegating account.
         {
-            test::jtx::Env env{*this, features};
+            Env env{*this, features};
             Account const gw{"gw"};
             Account const alice{"alice"};
             Account const bob{"bob"};
@@ -4832,20 +5391,23 @@ class Batch_test : public beast::unit_test::Suite
             env.close();
 
             auto const seq = env.seq(gw);
-            auto const batchFee = batch::calcBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
 
             auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze);
             jv1[sfDelegate] = bob.human();
             auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze);
             jv2[sfDelegate] = bob.human();
 
+            // Both inners are delegated to Bob, so Bob must provide the batch
+            // signature; jv2 still fails preclaim for lack of permission.
             auto const [txIDs, batchID] = submitBatch(
                 env,
                 tesSUCCESS,
                 batch::outer(gw, seq, batchFee, tfIndependent),
                 batch::Inner(jv1, seq + 1),
                 // terNO_DELEGATE_PERMISSION: not authorized to clear freeze
-                batch::Inner(jv2, seq + 2));
+                batch::Inner(jv2, seq + 2),
+                batch::Sig(bob));
             env.close();
 
             std::vector const testCases = {
@@ -5004,7 +5566,7 @@ class Batch_test : public beast::unit_test::Suite
                 batch::outer(alice, seq, batchFee, tfAllOrNothing),
                 batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
                 batch::Inner(pay(alice, bob, XRP(5)), seq + 2),
-                batch::Sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice));
+                batch::Sig(std::vector(kMaxBatchSigners + 1, bob)));
             XRPAmount const txBaseFee = getBaseFee(jtx);
             BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp));
         }
@@ -5022,6 +5584,304 @@ class Batch_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testStandaloneInnerBatchFlag(FeatureBitset features)
+    {
+        testcase("standalone tx with tfInnerBatchTxn rejected");
+
+        using namespace test::jtx;
+        using namespace std::literals;
+
+        // A standalone Payment with tfInnerBatchTxn must be rejected.
+        // Without proper guards this would bypass signature verification
+        // in preflight2.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            env.fund(XRP(10000), alice, bob);
+            env.close();
+
+            // Submit a normal Payment with tfInnerBatchTxn flag.
+            // preflight1 must reject with temINVALID_INNER_BATCH because
+            // the flag is set but no parentBatchId exists.
+            env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED));
+            env.close();
+
+            // Verify via direct apply path (bypassing RPC layer)
+            env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
+                // Construct a Payment STTx with tfInnerBatchTxn,
+                // empty signing pub key, and no signature — mimicking
+                // what an attacker would send to skip sig verification.
+                STTx const stx = STTx(ttPAYMENT, [&](auto& obj) {
+                    obj.setAccountID(sfAccount, alice.id());
+                    obj.setAccountID(sfDestination, bob.id());
+                    obj.setFieldAmount(sfAmount, XRP(1));
+                    obj.setFieldAmount(sfFee, XRP(0));
+                    obj.setFieldU32(sfSequence, env.seq(alice));
+                    obj.setFieldU32(sfFlags, tfInnerBatchTxn);
+                });
+
+                auto const result = xrpl::apply(env.app(), view, stx, TapNone, j);
+                // Must NOT be applied — signature was never checked
+                BEAST_EXPECT(!result.applied);
+                return false;
+            });
+        }
+    }
+
+    void
+    testOuterBinding(FeatureBitset features)
+    {
+        testcase("outer binding");
+
+        using namespace test::jtx;
+
+        // Signatures captured from one outer account cannot be replayed
+        // under a different outer account.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const carol = Account("carol");
+            auto const eve = Account("eve");
+            env.fund(XRP(10000), alice, bob, carol, eve);
+            env.close();
+
+            auto const preCarol = env.balance(carol);
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+
+            auto const aliceSeq = env.seq(alice);
+            auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
+            auto jt1 = env.jt(
+                batch::outer(alice, aliceSeq, batchFee1, tfOnlyOne),
+                batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
+                batch::Inner(pay(carol, alice, XRP(50)), carolSeq),
+                batch::Sig(bob, carol));
+
+            auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
+
+            env(jt1, Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
+            BEAST_EXPECT(env.seq(carol) == carolSeq);
+
+            auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
+            auto jt2 = env.jtnofill(
+                batch::outer(eve, env.seq(eve), batchFee2, tfOnlyOne),
+                batch::Inner(pay(bob, alice, XRP(100)), bobSeq),
+                batch::Inner(pay(carol, alice, XRP(50)), carolSeq));
+
+            jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
+            env(jt2.jv, Ter(telENV_RPC_FAILED));
+            env.close();
+
+            BEAST_EXPECT(env.seq(carol) == carolSeq);
+            BEAST_EXPECT(env.balance(carol) == preCarol);
+        }
+
+        // Signatures are bound to the outer sequence; replaying them
+        // at a higher sequence must fail.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const carol = Account("carol");
+            env.fund(XRP(10000), alice, bob, carol);
+            env.close();
+
+            auto const preCarol = env.balance(carol);
+            auto const bobSeq = env.seq(bob);
+            auto const carolSeq = env.seq(carol);
+
+            auto const aliceSeq1 = env.seq(alice);
+            auto const batchFee1 = batch::calcBatchFee(env, 2, 2);
+            auto jt1 = env.jt(
+                batch::outer(alice, aliceSeq1, batchFee1, tfOnlyOne),
+                batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
+                batch::Inner(pay(carol, alice, XRP(500)), carolSeq),
+                batch::Sig(bob, carol));
+
+            auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName];
+
+            env(jt1, Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(env.seq(bob) == bobSeq + 1);
+            BEAST_EXPECT(env.seq(carol) == carolSeq);
+
+            auto const batchFee2 = batch::calcBatchFee(env, 2, 2);
+            auto jt2 = env.jtnofill(
+                batch::outer(alice, env.seq(alice), batchFee2, tfOnlyOne),
+                batch::Inner(pay(bob, alice, XRP(500)), bobSeq),
+                batch::Inner(pay(carol, alice, XRP(500)), carolSeq));
+
+            jt2.jv[sfBatchSigners.jsonName] = capturedSigners;
+            env(jt2.jv, Ter(telENV_RPC_FAILED));
+            env.close();
+
+            BEAST_EXPECT(env.balance(carol) == preCarol);
+            BEAST_EXPECT(env.seq(carol) == carolSeq);
+        }
+
+        // Multi-signed batch signer entries are bound to their account;
+        // reusing inner signatures under a different batch signer must fail.
+        {
+            Env env{*this, features};
+
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const carol = Account("carol");
+            auto const dave = Account("dave");
+            auto const elsa = Account("elsa");
+            env.fund(XRP(10000), alice, bob, carol, dave, elsa);
+            env.close();
+
+            env(signers(bob, 2, {{dave, 1}, {elsa, 1}}));
+            env.close();
+            env(signers(carol, 2, {{dave, 1}, {elsa, 1}}));
+            env.close();
+
+            auto const seq = env.seq(alice);
+            auto const batchFee = batch::calcBatchFee(env, 3, 2);
+            auto jt1 = env.jt(
+                batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
+                batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)),
+                batch::Msig(bob, {dave, elsa}),
+                Ter(tesSUCCESS));
+
+            auto const bobSignerEntry = jt1.jv[sfBatchSigners.jsonName][0u];
+
+            env(jt1, Ter(tesSUCCESS));
+            env.close();
+
+            auto const seq2 = env.seq(alice);
+            auto const batchFee2 = batch::calcBatchFee(env, 3, 2);
+            auto jt2 = env.jtnofill(
+                batch::outer(alice, seq2, batchFee2, tfAllOrNothing),
+                batch::Inner(pay(alice, carol, XRP(10)), seq2 + 1),
+                batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol)));
+
+            json::Value carolSigner;
+            carolSigner[sfBatchSigner.jsonName][jss::Account] = carol.human();
+            carolSigner[sfBatchSigner.jsonName][jss::SigningPubKey] = "";
+            carolSigner[sfBatchSigner.jsonName][sfSigners.jsonName] =
+                bobSignerEntry[sfBatchSigner.jsonName][sfSigners.jsonName];
+
+            jt2.jv[sfBatchSigners.jsonName][0u] = carolSigner;
+            env(jt2.jv, Ter(telENV_RPC_FAILED));
+            env.close();
+        }
+    }
+
+    void
+    testUnsortedBatchSigners(FeatureBitset features)
+    {
+        testcase("unsorted batch signers");
+
+        using namespace test::jtx;
+
+        Env env{*this, features};
+
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const carol = Account("carol");
+        env.fund(XRP(10000), alice, bob, carol);
+        env.close();
+
+        auto const seq = env.seq(alice);
+        auto const bobSeq = env.seq(bob);
+        auto const carolSeq = env.seq(carol);
+        auto const batchFee = batch::calcBatchFee(env, 2, 2);
+
+        auto jt = env.jt(
+            batch::outer(alice, seq, batchFee, tfAllOrNothing),
+            batch::Inner(pay(bob, alice, XRP(10)), bobSeq),
+            batch::Inner(pay(carol, alice, XRP(5)), carolSeq),
+            batch::Sig(bob, carol));
+
+        auto const s0 = jt.jv[sfBatchSigners.jsonName][0u];
+        auto const s1 = jt.jv[sfBatchSigners.jsonName][1u];
+        jt.jv[sfBatchSigners.jsonName][0u] = s1;
+        jt.jv[sfBatchSigners.jsonName][1u] = s0;
+
+        env(jt.jv, Ter(temBAD_SIGNER));
+        env.close();
+    }
+
+    void
+    testBatchSigCache(FeatureBitset features)
+    {
+        testcase("batch signature caching");
+
+        using namespace test::jtx;
+
+        // Mirrors apply.cpp's file-local kSfSiggood (the standard signature-good
+        // cache); batch signer sigs are now verified and cached alongside the
+        // outer signature in checkSign/checkValidity.
+        constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2;
+
+        // Valid batch: alice (outer) + an inner from bob, who co-signs.
+        auto buildValidBatch = [](Env& env) {
+            auto const alice = Account("alice");
+            auto const bob = Account("bob");
+            auto const seq = env.seq(alice);
+            auto const batchFee = batch::calcBatchFee(env, 1, 2);
+            return env.jt(
+                batch::outer(alice, seq, batchFee, tfAllOrNothing),
+                batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
+                batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)),
+                batch::Sig(bob));
+        };
+
+        // WRITE: a valid batch records "good" on its tx id.
+        {
+            Env env{*this, features};
+            env.fund(XRP(10000), Account("alice"), Account("bob"));
+            env.close();
+
+            auto jt = buildValidBatch(env);
+            auto const txid = jt.stx->getTransactionID();
+
+            BEAST_EXPECT(!any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
+            env(jt, Ter(tesSUCCESS));
+            BEAST_EXPECT(any(env.app().getHashRouter().getFlags(txid) & kSfSiggood));
+            env.close();
+        }
+
+        // READ: corrupt only a signer's signature (outer sig + signer key
+        // untouched), so just the crypto would fail. Caught when uncached...
+        {
+            Env env{*this, features};
+            env.fund(XRP(10000), Account("alice"), Account("bob"));
+            env.close();
+
+            auto jt = buildValidBatch(env);
+            jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
+                "00";
+            env(jt.jv, Ter(telENV_RPC_FAILED));
+            env.close();
+        }
+        {
+            // ...but a planted "good" skips the crypto, so it applies.
+            Env env{*this, features};
+            env.fund(XRP(10000), Account("alice"), Account("bob"));
+            env.close();
+
+            auto jt = buildValidBatch(env);
+            jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] =
+                "00";
+            auto const txid = STTx{parse(jt.jv)}.getTransactionID();
+            env.app().getHashRouter().setFlags(txid, kSfSiggood);
+            env(jt.jv, Ter(tesSUCCESS));
+            env.close();
+        }
+    }
+
     void
     testWithFeats(FeatureBitset features)
     {
@@ -5038,6 +5898,7 @@ class Batch_test : public beast::unit_test::Suite
         testIndependent(features);
         testInnerSubmitRPC(features);
         testAccountActivation(features);
+        testCheckAllSignatures(features);
         testAccountSet(features);
         testAccountDelete(features);
         testLoan(features);
@@ -5053,8 +5914,13 @@ class Batch_test : public beast::unit_test::Suite
         testBatchTxQueue(features);
         testBatchNetworkOps(features);
         testBatchDelegate(features);
+        testBatchDelegateConsent(features);
         testValidateRPCResponse(features);
         testBatchCalculateBaseFee(features);
+        testStandaloneInnerBatchFlag(features);
+        testOuterBinding(features);
+        testUnsortedBatchSigners(features);
+        testBatchSigCache(features);
     }
 
 public:
@@ -5064,7 +5930,6 @@ public:
         using namespace test::jtx;
 
         auto const sa = testableAmendments();
-        testWithFeats(sa - fixBatchInnerSigs);
         testWithFeats(sa);
     }
 };
diff --git a/src/test/app/ConfidentialTransferExtended_test.cpp b/src/test/app/ConfidentialTransferExtended_test.cpp
index 0aee7516c9..953325a6e9 100644
--- a/src/test/app/ConfidentialTransferExtended_test.cpp
+++ b/src/test/app/ConfidentialTransferExtended_test.cpp
@@ -1945,7 +1945,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env.close();
 
             auto const bobSeq = env.seq(bob);
-            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
 
             // jv1: proof against spending balance 100
             auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 60}, bobSeq + 1);
@@ -1957,6 +1957,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
                 batch::Inner(jv1, bobSeq + 1),
                 batch::Inner(jv2, bobSeq + 2),
+                batch::Sig(dave),
                 Ter(tesSUCCESS));
             env.close();
 
@@ -1981,7 +1982,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env.close();
 
             auto const bobSeq = env.seq(bob);
-            auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
 
             // jv1: proof against spending balance 100.
             auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1);
@@ -1994,6 +1995,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
                 batch::Inner(jv1, bobSeq + 1),
                 batch::Inner(jv2, bobSeq + 2),
+                batch::Sig(dave),
                 Ter(tesSUCCESS));
             env.close();
 
@@ -2029,7 +2031,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
 
             auto const bobSeq = env.seq(bob);
             auto const carolSeq = env.seq(carol);
-            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
 
             // jv1: direct send from carol (valid proof).
             auto const jv1 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
@@ -2040,7 +2042,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
                 batch::Inner(jv1, carolSeq),
                 batch::Inner(jv2, bobSeq + 1),
-                batch::Sig(carol),
+                batch::Sig(carol, dave),
                 Ter(tesSUCCESS));
             env.close();
 
@@ -2066,7 +2068,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             // Bob does not grant dave any permissions.
             auto const bobSeq = env.seq(bob);
             auto const carolSeq = env.seq(carol);
-            auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
+            auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
 
             auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
             jv1[jss::Delegate] = dave.human();
@@ -2075,7 +2077,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
                 batch::Inner(jv1, bobSeq + 1),
                 batch::Inner(jv2, carolSeq),
-                batch::Sig(carol),
+                batch::Sig(carol, dave),
                 Ter(tesSUCCESS));
             env.close();
 
@@ -2093,8 +2095,9 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
         testcase("Test batch delegated send with delegate as outer account");
         using namespace test::jtx;
 
-        // Dave has delegation permission, but the inner Account is bob.
-        // Without bob's BatchSigner, the batch is rejected.
+        // Dave holds bob's ConfidentialMPTSend delegation and is the outer batch
+        // signer, so dave's outer signature consents to the delegated inner.
+        // The batch applies.
         {
             Env env{*this, features};
             Account const alice("alice");
@@ -2119,11 +2122,11 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
                 batch::Inner(jv1, bobSeq),
                 batch::Inner(jv2, daveSeq + 1),
-                Ter(temBAD_SIGNER));
+                Ter(tesSUCCESS));
             env.close();
 
-            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
-            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
+            BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 60);
+            BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
         }
 
         // Dave submits a mixed batch: bob signs inner tx1, and
@@ -2163,8 +2166,10 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 70);
         }
 
-        // Verify the delegator Bob's BatchSigner does not bypass the missing delegation permission.
-        // The delegated inner send fails.
+        // The delegated inner's required signer is the delegate (dave), not bob.
+        // Bob signs but is not a required signer, so the batch is rejected as an
+        // extra signer. The delegator's signature cannot stand in for the
+        // delegate's.
         {
             Env env{*this, features};
             Account const alice("alice");
@@ -2189,7 +2194,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
                 batch::Inner(jv1, bobSeq),
                 batch::Inner(jv2, carolSeq),
                 batch::Sig(bob, carol),
-                Ter(tesSUCCESS));
+                Ter(temBAD_SIGNER));
             env.close();
 
             // jv1 fails before jv2 is attempted.
@@ -2257,7 +2262,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase
             batch::Inner(jv4, frankSeq),
             batch::Inner(jv5, frankSeq + 1),
             batch::Inner(jv6, bobSeq + 2),
-            batch::Sig(bob, carol, frank),
+            batch::Sig(erin, frank),
             Ter(tesSUCCESS));
         env.close();
 
diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp
index 2422db05de..ed99817f34 100644
--- a/src/test/app/LedgerReplay_test.cpp
+++ b/src/test/app/LedgerReplay_test.cpp
@@ -1,12 +1,14 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -36,6 +38,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -68,7 +72,7 @@ namespace xrpl::test {
 struct LedgerReplay_test : public beast::unit_test::Suite
 {
     void
-    run() override
+    testReplayLedger()
     {
         testcase("Replay ledger");
 
@@ -91,6 +95,45 @@ struct LedgerReplay_test : public beast::unit_test::Suite
 
         BEAST_EXPECT(replayed->header().hash == lastClosed->header().hash);
     }
+
+    void
+    testReplayBatchLedger()
+    {
+        testcase("Replay ledger with batch transactions");
+
+        using namespace jtx;
+
+        Env env(*this, testableAmendments());
+
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        env.fund(XRP(100000), alice, bob);
+        env.close();
+
+        auto const seq = env.seq(alice);
+        auto const batchFee = batch::calcBatchFee(env, 0, 2);
+        env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
+            batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
+            batch::Inner(pay(alice, bob, XRP(2)), seq + 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        LedgerMaster& ledgerMaster = env.app().getLedgerMaster();
+        auto const lastClosed = ledgerMaster.getClosedLedger();
+        auto const lastClosedParent = ledgerMaster.getLedgerByHash(lastClosed->header().parentHash);
+
+        auto const replayed = buildLedger(
+            LedgerReplay(lastClosedParent, lastClosed), TapNone, env.app(), env.journal);
+
+        BEAST_EXPECT(replayed->header().hash == lastClosed->header().hash);
+    }
+
+    void
+    run() override
+    {
+        testReplayLedger();
+        testReplayBatchLedger();
+    }
 };
 
 enum class InboundLedgersBehavior {
diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp
index 2d10eb4beb..91eb26da7e 100644
--- a/src/test/app/TxQ_test.cpp
+++ b/src/test/app/TxQ_test.cpp
@@ -61,8 +61,7 @@ namespace xrpl::test {
 
 class TxQPosNegFlows_test : public beast::unit_test::Suite
 {
-    // Same as corresponding values from TxQ.h
-    static constexpr FeeLevel64 kBaseFeeLevel{256};
+    static constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel};
     static constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500;
 
     static void
diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h
index 0e35e6b9ec..34532b17f7 100644
--- a/src/test/jtx/TestHelpers.h
+++ b/src/test/jtx/TestHelpers.h
@@ -731,8 +731,8 @@ create(jtx::Account const& account, jtx::Account const& dest, STAmount const& se
 
 }  // namespace check
 
-static constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel};
-static constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500;
+inline constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel};
+inline constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500;
 
 inline uint256
 getCheckIndex(AccountID const& account, std::uint32_t uSequence)
diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h
index 0e78d409fa..4d564f150a 100644
--- a/src/test/jtx/batch.h
+++ b/src/test/jtx/batch.h
@@ -58,7 +58,6 @@ class Inner
 {
 private:
     json::Value txn_;
-    std::uint32_t seq_;
     std::optional ticket_;
 
 public:
@@ -66,10 +65,10 @@ public:
         json::Value txn,
         std::uint32_t const& sequence,
         std::optional const& ticket = std::nullopt)
-        : txn_(std::move(txn)), seq_(sequence), ticket_(ticket)
+        : txn_(std::move(txn)), ticket_(ticket)
     {
         txn_[jss::SigningPubKey] = "";
-        txn_[jss::Sequence] = seq_;
+        txn_[jss::Sequence] = sequence;
         txn_[jss::Fee] = "0";
         txn_[jss::Flags] = txn_[jss::Flags].asUInt() | tfInnerBatchTxn;
 
diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp
index 66ca0c7d54..b1061f65a3 100644
--- a/src/test/jtx/impl/batch.cpp
+++ b/src/test/jtx/impl/batch.cpp
@@ -99,7 +99,13 @@ Sig::operator()(Env& env, JTx& jt) const
         jo[jss::SigningPubKey] = strHex(e.sig.pk().slice());
 
         Serializer msg;
-        serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs());
+        serializeBatch(
+            msg,
+            stx.getAccountID(sfAccount),
+            stx.getSeqValue(),
+            stx.getFlags(),
+            stx.getBatchTransactionIDs());
+        finishMultiSigningData(e.acct.id(), msg);
         // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
         auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice());
         jo[sfTxnSignature.getJsonName()] = strHex(Slice{sig.data(), sig.size()});
@@ -137,7 +143,13 @@ Msig::operator()(Env& env, JTx& jt) const
         iso[jss::SigningPubKey] = strHex(e.sig.pk().slice());
 
         Serializer msg;
-        serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs());
+        serializeBatch(
+            msg,
+            stx.getAccountID(sfAccount),
+            stx.getSeqValue(),
+            stx.getFlags(),
+            stx.getBatchTransactionIDs());
+        msg.addBitString(master.id());
         finishMultiSigningData(e.acct.id(), msg);
         // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
         auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice());
diff --git a/src/test/rpc/Feature_test.cpp b/src/test/rpc/Feature_test.cpp
index 8cda5965cb..a36e51cb6f 100644
--- a/src/test/rpc/Feature_test.cpp
+++ b/src/test/rpc/Feature_test.cpp
@@ -134,7 +134,7 @@ class Feature_test : public beast::unit_test::Suite
         // or removed, swap out for any other feature.
         BEAST_EXPECT(
             featureToName(fixRemoveNFTokenAutoTrustLine) == "fixRemoveNFTokenAutoTrustLine");
-        BEAST_EXPECT(featureToName(featureBatch) == "Batch");
+        BEAST_EXPECT(featureToName(featureBatchV1_1) == "BatchV1_1");
         BEAST_EXPECT(featureToName(featureDID) == "DID");
         BEAST_EXPECT(featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields");
         BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow");
diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp
index 5ea79c3996..0d00360a58 100644
--- a/src/test/rpc/Simulate_test.cpp
+++ b/src/test/rpc/Simulate_test.cpp
@@ -420,6 +420,20 @@ class Simulate_test : public beast::unit_test::Suite
             BEAST_EXPECT(
                 resp[jss::result][jss::error_message] == "Transaction should not be signed.");
         }
+        {
+            // tfInnerBatchTxn flag on top-level transaction
+            json::Value params;
+            json::Value txJson{json::ValueType::Object};
+            txJson[jss::TransactionType] = jss::AccountSet;
+            txJson[jss::Account] = env.master.human();
+            txJson[jss::Flags] = tfInnerBatchTxn;
+            params[jss::tx_json] = txJson;
+
+            auto const resp = env.rpc("json", "simulate", to_string(params));
+            BEAST_EXPECT(
+                resp[jss::result][jss::error_message] ==
+                "tfInnerBatchTxn flag is not allowed on top-level transactions.");
+        }
     }
 
     void
@@ -476,7 +490,7 @@ class Simulate_test : public beast::unit_test::Suite
         auto jt = env.jtnofill(
             batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
             batch::Inner(pay(alice, bob, XRP(10)), seq + 1),
-            batch::Inner(pay(alice, bob, XRP(10)), seq + 1));
+            batch::Inner(pay(alice, bob, XRP(10)), seq + 2));
 
         jt.jv.removeMember(jss::TxnSignature);
         json::Value params;
diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp
index 2eb64ecaf9..038a7be4b7 100644
--- a/src/xrpld/app/ledger/detail/BuildLedger.cpp
+++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp
@@ -17,6 +17,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -233,7 +234,15 @@ buildLedger(
         j,
         [&](OpenView& accum, std::shared_ptr const& built) {
             for (auto& tx : replayData.orderedTxns())
+            {
+                // Inner batch transactions are applied as part of their outer
+                // Batch transaction, never on their own. Skip them here so they
+                // are not re-applied a second time outside of the batch during
+                // replay.
+                if (tx.second->isFlag(tfInnerBatchTxn))
+                    continue;
                 applyTransaction(app, accum, *tx.second, false, applyFlags, j);
+            }
         });
 }
 
diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp
index 1802441a66..82445f3625 100644
--- a/src/xrpld/app/misc/NetworkOPs.cpp
+++ b/src/xrpld/app/misc/NetworkOPs.cpp
@@ -1253,8 +1253,9 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans)
         return;
     }
 
-    // Enforce Network bar for batch txn
-    if (iTrans->isFlag(tfInnerBatchTxn) && ledgerMaster_.getValidatedRules().enabled(featureBatch))
+    // Reject any transaction with the tfInnerBatchTxn flag at the network
+    // boundary, regardless of amendment state.
+    if (iTrans->isFlag(tfInnerBatchTxn))
     {
         JLOG(journal_.error()) << "Submitted transaction invalid: tfInnerBatchTxn flag present.";
         return;
@@ -1320,7 +1321,7 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction)
     // under no circumstances will we ever accept an inner txn within a batch
     // txn from the network.
     auto const sttx = *transaction->getSTransaction();
-    if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch))
+    if (sttx.isFlag(tfInnerBatchTxn))
     {
         transaction->setStatus(TransStatus::INVALID);
         transaction->setResult(temINVALID_FLAG);
diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp
index ec5ac569d6..7e57302d23 100644
--- a/src/xrpld/app/misc/detail/TxQ.cpp
+++ b/src/xrpld/app/misc/detail/TxQ.cpp
@@ -25,6 +25,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -390,6 +391,12 @@ TxQ::canBeHeld(
     std::optional const& replacementIter,
     std::scoped_lock const& lock)
 {
+    // A Batch is never queued: its inner transactions can change the sequence
+    // numbers of multiple accounts, which the TxQ's per-account model cannot
+    // forecast. It must apply straight to the open ledger or not at all.
+    if (tx.getTxnType() == ttBATCH)
+        return telCAN_NOT_QUEUE;
+
     // PreviousTxnID is deprecated and should never be used.
     // AccountTxnID is not supported by the transaction
     // queue yet, but should be added in the future.
diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp
index e1d7d23215..5ac61cfd91 100644
--- a/src/xrpld/overlay/detail/PeerImp.cpp
+++ b/src/xrpld/overlay/detail/PeerImp.cpp
@@ -1309,7 +1309,7 @@ PeerImp::handleTransaction(
         // Charge strongly for attempting to relay a txn with tfInnerBatchTxn
         // LCOV_EXCL_START
         /*
-           There is no need to check whether the featureBatch amendment is
+           There is no need to check whether the featureBatchV1_1 amendment is
            enabled.
 
            * If the `tfInnerBatchTxn` flag is set, and the amendment is
@@ -2873,7 +2873,7 @@ PeerImp::checkTransaction(
         // charge strongly for relaying batch txns
         // LCOV_EXCL_START
         /*
-           There is no need to check whether the featureBatch amendment is
+           There is no need to check whether the featureBatchV1_1 amendment is
            enabled.
 
            * If the `tfInnerBatchTxn` flag is set, and the amendment is
diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp
index 7ee28c4886..2a6c2280e3 100644
--- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp
+++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp
@@ -26,6 +26,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -355,6 +356,13 @@ doSimulate(RPC::JsonContext& context)
         return RPC::makeError(RpcNotImpl);
     }
 
+    // Reject transactions with the tfInnerBatchTxn flag.
+    if (stTx->isFlag(tfInnerBatchTxn))
+    {
+        return RPC::makeError(
+            RpcInvalidParams, "tfInnerBatchTxn flag is not allowed on top-level transactions.");
+    }
+
     std::string reason;
     auto transaction = std::make_shared(stTx, reason, context.app);
     // Actually run the transaction through the transaction processor

From ea13be81b7037ce0a03c706e5b916b0beb581496 Mon Sep 17 00:00:00 2001
From: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 1 Jul 2026 15:21:23 +0200
Subject: [PATCH 029/100] feat: Add an invariant to ensure object deletion also
 deletes its pseudo-account (#7445)

Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
---
 include/xrpl/tx/invariants/InvariantCheck.h  |  26 +++-
 src/libxrpl/tx/invariants/InvariantCheck.cpp |  95 +++++++++++--
 src/test/app/Invariants_test.cpp             | 142 +++++++++++++++++--
 3 files changed, 242 insertions(+), 21 deletions(-)

diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h
index 8931d189fd..ba91cba064 100644
--- a/include/xrpl/tx/invariants/InvariantCheck.h
+++ b/include/xrpl/tx/invariants/InvariantCheck.h
@@ -375,16 +375,35 @@ public:
  */
 class ValidAmounts
 {
-    std::vector> afterEntries_;
+    std::vector afterEntries_;
 
 public:
     void
-    visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&);
+    visitEntry(bool, SLE::const_ref, SLE::const_ref);
 
     [[nodiscard]] bool
     finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
 };
 
+/*
+ * Verify that when an object with an associated pseudo-account is deleted,
+ * its pseudo-account is also deleted.
+ *
+ * The reverse (pseudo-account deleted → object deleted) is enforced by
+ * AccountRootsDeletedClean via getPseudoAccountFields().
+ */
+class ObjectHasPseudoAccount
+{
+public:
+    void
+    visitEntry(bool, SLE::const_ref, SLE::const_ref);
+
+    [[nodiscard]] bool
+    finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
+
+private:
+    std::vector deletedObjSles_;
+};
 // additional invariant checks can be declared above and then added to this
 // tuple
 using InvariantChecks = std::tuple<
@@ -416,7 +435,8 @@ using InvariantChecks = std::tuple<
     ValidConfidentialMPToken,
     ValidMPTPayment,
     ValidAmounts,
-    ValidMPTTransfer>;
+    ValidMPTTransfer,
+    ObjectHasPseudoAccount>;
 
 /**
  * @brief get a tuple of all invariant checks
diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp
index 308342da74..32df44a96b 100644
--- a/src/libxrpl/tx/invariants/InvariantCheck.cpp
+++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp
@@ -35,6 +35,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
@@ -63,6 +64,23 @@ hasPrivilege(STTx const& tx, Privilege priv)
 #undef TRANSACTION
 #pragma pop_macro("TRANSACTION")
 
+// Returns the human-readable name of a ledger entry's type, falling back to
+// the numeric type if the format is somehow unknown.
+static std::string
+ledgerEntryTypeName(SLE const& sle)
+{
+    auto const item = LedgerFormats::getInstance().findByType(sle.getType());
+
+    if (item == nullptr)
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::ledgerEntryTypeName : ledger entry has no known ledger format");
+        return std::to_string(sle.getType());
+        // LCOV_EXCL_STOP
+    }
+    return item->getName();
+}
+
 void
 TransactionFeeCheck::visitEntry(bool, SLE::const_ref, SLE::const_ref)
 {
@@ -457,16 +475,8 @@ AccountRootsDeletedClean::finalize(
         if (auto const sle = view.read(keylet))
         {
             // Finding the object is bad
-            auto const typeName = [&sle]() {
-                auto item = LedgerFormats::getInstance().findByType(sle->getType());
-
-                if (item != nullptr)
-                    return item->getName();
-                return std::to_string(sle->getType());
-            }();
-
-            JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName
-                            << " object";
+            JLOG(j.fatal()) << "Invariant failed: account deletion left behind a "
+                            << ledgerEntryTypeName(*sle) << " object";
             // The comment above starting with "assert(enforce)" explains this
             // assert.
             XRPL_ASSERT(
@@ -1070,4 +1080,69 @@ ValidAmounts::finalize(
     return true;
 }
 
+void
+ObjectHasPseudoAccount::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
+{
+    if (!isDelete)
+        return;
+
+    // Before should never be null when isDelete = true
+    if (!before)
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state");
+        return;
+        // LCOV_EXCL_STOP
+    }
+
+    switch (before->getType())
+    {
+        case ltAMM:
+        case ltVAULT:
+        case ltLOAN_BROKER:
+            deletedObjSles_.push_back(before);
+            break;
+        default:
+            return;
+    }
+}
+
+[[nodiscard]] bool
+ObjectHasPseudoAccount::finalize(
+    STTx const&,
+    TER const,
+    XRPAmount const,
+    ReadView const& view,
+    beast::Journal const& j) const
+{
+    if (!view.rules().enabled(fixCleanup3_3_0))
+        return true;
+
+    if (deletedObjSles_.empty())
+        return true;
+
+    bool failed = false;
+    for (auto const& sle : deletedObjSles_)
+    {
+        if (!sle->isFieldPresent(sfAccount))
+        {
+            JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
+                            << " is missing pseudo-account field";
+            failed = true;
+            continue;
+        }
+
+        // The pseudo-account must NOT exist on the ledger after the object is deleted.
+        if (view.exists(keylet::account(sle->getAccountID(sfAccount))))
+        {
+            JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
+                            << " without deleting its pseudo-account";
+            failed = true;
+        }
+    }
+
+    return !failed;
+}
+
 }  // namespace xrpl
diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp
index 9fde52ecb2..079ff9df88 100644
--- a/src/test/app/Invariants_test.cpp
+++ b/src/test/app/Invariants_test.cpp
@@ -2829,7 +2829,8 @@ class Invariants_test : public beast::unit_test::Suite
             });
 
         doInvariantCheck(
-            {"vault updated by a wrong transaction type"},
+            {"vault updated by a wrong transaction type",
+             "deleted Vault without deleting its pseudo-account"},
             [&](Account const& a1, Account const& a2, ApplyContext& ac) {
                 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
                 auto sleVault = ac.view().peek(keylet);
@@ -2840,7 +2841,7 @@ class Invariants_test : public beast::unit_test::Suite
             },
             XRPAmount{},
             STTx{ttPAYMENT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
             [&](Account const& a1, Account const& a2, Env& env) {
                 Vault const vault{env};
                 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
@@ -2877,6 +2878,7 @@ class Invariants_test : public beast::unit_test::Suite
                 auto const vaultPage = ac.view().dirInsert(
                     keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
                 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+                sleVault->setAccountID(sfAccount, a1.id());
                 ac.view().insert(sleVault);
                 return true;
             },
@@ -2885,7 +2887,8 @@ class Invariants_test : public beast::unit_test::Suite
             {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
 
         doInvariantCheck(
-            {"vault deleted by a wrong transaction type"},
+            {"vault deleted by a wrong transaction type",
+             "deleted Vault without deleting its pseudo-account"},
             [&](Account const& a1, Account const& a2, ApplyContext& ac) {
                 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
                 auto sleVault = ac.view().peek(keylet);
@@ -2896,7 +2899,7 @@ class Invariants_test : public beast::unit_test::Suite
             },
             XRPAmount{},
             STTx{ttVAULT_SET, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
             [&](Account const& a1, Account const& a2, Env& env) {
                 Vault const vault{env};
                 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
@@ -2905,7 +2908,8 @@ class Invariants_test : public beast::unit_test::Suite
             });
 
         doInvariantCheck(
-            {"vault operation updated more than single vault"},
+            {"vault operation updated more than single vault",
+             "deleted Vault without deleting its pseudo-account"},
             [&](Account const& a1, Account const& a2, ApplyContext& ac) {
                 {
                     auto const keylet = keylet::vault(a1.id(), ac.view().seq());
@@ -2925,7 +2929,7 @@ class Invariants_test : public beast::unit_test::Suite
             },
             XRPAmount{},
             STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
             [&](Account const& a1, Account const& a2, Env& env) {
                 Vault const vault{env};
                 {
@@ -2949,6 +2953,7 @@ class Invariants_test : public beast::unit_test::Suite
                     auto const vaultPage = ac.view().dirInsert(
                         keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
                     sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+                    sleVault->setAccountID(sfAccount, a.id());
                     ac.view().insert(sleVault);
                 };
                 insertVault(a1);
@@ -2960,7 +2965,8 @@ class Invariants_test : public beast::unit_test::Suite
             {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
 
         doInvariantCheck(
-            {"deleted vault must also delete shares"},
+            {"deleted vault must also delete shares",
+             "deleted Vault without deleting its pseudo-account"},
             [&](Account const& a1, Account const& a2, ApplyContext& ac) {
                 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
                 auto sleVault = ac.view().peek(keylet);
@@ -2971,7 +2977,7 @@ class Invariants_test : public beast::unit_test::Suite
             },
             XRPAmount{},
             STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
             [&](Account const& a1, Account const& a2, Env& env) {
                 Vault const vault{env};
                 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
@@ -5176,6 +5182,125 @@ class Invariants_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testObjectHasPseudoAccount()
+    {
+        testcase << "object has pseudo-account";
+        using namespace jtx;
+
+        auto const amendments = defaultAmendments() | fixCleanup3_3_0;
+
+        // Vault: object deleted without its pseudo-account
+        {
+            Keylet vaultKeylet = keylet::amendments();
+            doInvariantCheck(
+                Env{*this, amendments},
+                {{"deleted Vault without deleting its pseudo-account"}},
+                [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(vaultKeylet);
+                    if (!sle)
+                        return false;
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttVAULT_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&vaultKeylet](Account const& a1, Account const&, Env& env) {
+                    Vault const vault{env};
+                    auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                    env(tx);
+                    vaultKeylet = keylet;
+                    return true;
+                });
+        }
+
+        // AMM: object deleted without its pseudo-account
+        {
+            uint256 ammID{};
+            Account const gw{"gw"};
+            doInvariantCheck(
+                Env{*this, amendments},
+                {{"deleted AMM without deleting its pseudo-account"}},
+                [&ammID](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(keylet::amm(ammID));
+                    if (!sle)
+                        return false;
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttAMM_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&ammID, &gw](Account const&, Account const&, Env& env) {
+                    env.fund(XRP(1'000), gw);
+                    AMM const amm(env, gw, XRP(100), gw["USD"](100));
+                    ammID = amm.ammID();
+                    return true;
+                });
+        }
+
+        // LoanBroker: object deleted without its pseudo-account
+        {
+            Keylet loanBrokerKeylet = keylet::amendments();
+            doInvariantCheck(
+                Env{*this, amendments},
+                {{"deleted LoanBroker without deleting its pseudo-account"}},
+                [&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(loanBrokerKeylet);
+                    if (!sle)
+                        return false;
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
+                    PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+                    loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
+                    return BEAST_EXPECT(env.le(loanBrokerKeylet));
+                });
+        }
+
+        // Deleted object missing sfAccount field (defensive check).
+        // Manually construct the view to place a vault SLE without
+        // sfAccount into the base ledger, then erase it.
+        {
+            Env env{*this, amendments};
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            env.close();
+
+            OpenView ov{*env.current()};
+
+            auto const vaultKeylet = keylet::vault(a1.id(), ov.seq());
+            auto sleVault = std::make_shared(vaultKeylet);
+            sleVault->makeFieldAbsent(sfAccount);
+            ov.rawInsert(sleVault);
+
+            STTx const tx{ttVAULT_DELETE, [](STObject&) {}};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            auto sle = ac.view().peek(vaultKeylet);
+            if (!BEAST_EXPECT(sle))
+                return;
+            ac.view().erase(sle);
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
+            BEAST_EXPECT(result == tecINVARIANT_FAILED);
+            BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
+        }
+    }
+
     void
     testConfidentialMPTTransfer()
     {
@@ -5458,6 +5583,7 @@ public:
         testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
         testVaultComputeCoarsestScale();
         testAMM();
+        testObjectHasPseudoAccount();
     }
 };
 

From 6aed3bb71d2820f081ce2a22666e456420a814b2 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Wed, 1 Jul 2026 14:28:14 +0100
Subject: [PATCH 030/100] chore: Update pre-commit hooks && actions (#7686)

---
 .github/workflows/pre-commit.yml                 | 2 +-
 .github/workflows/publish-docs.yml               | 2 +-
 .github/workflows/reusable-build-test-config.yml | 2 +-
 .github/workflows/reusable-clang-tidy.yml        | 2 +-
 .github/workflows/upload-conan-deps.yml          | 2 +-
 .pre-commit-config.yaml                          | 6 +++---
 docs/0001-negative-unl/README.md                 | 2 +-
 7 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 0363534af5..1acd28208e 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -14,7 +14,7 @@ on:
 jobs:
   # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
   run-hooks:
-    uses: XRPLF/actions/.github/workflows/pre-commit.yml@e06d4138c9ec8dceeb7c818645faa38087ea9e3d
+    uses: XRPLF/actions/.github/workflows/pre-commit.yml@1bde119a1ab71305ba5d3716e7a82cea1c7bdede
     with:
       runs_on: ubuntu-latest
       container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
index bfa8d2e79c..1ac8d61655 100644
--- a/.github/workflows/publish-docs.yml
+++ b/.github/workflows/publish-docs.yml
@@ -47,7 +47,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
+        uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
         with:
           enable_ccache: false
 
diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml
index 0cb0219d72..e7a88a0e66 100644
--- a/.github/workflows/reusable-build-test-config.yml
+++ b/.github/workflows/reusable-build-test-config.yml
@@ -113,7 +113,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
+        uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
         with:
           enable_ccache: ${{ inputs.ccache_enabled }}
 
diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml
index b04847e137..5528f3452e 100644
--- a/.github/workflows/reusable-clang-tidy.yml
+++ b/.github/workflows/reusable-clang-tidy.yml
@@ -43,7 +43,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
+        uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
         with:
           enable_ccache: false
 
diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml
index 88b364c2b1..22ad36d98f 100644
--- a/.github/workflows/upload-conan-deps.yml
+++ b/.github/workflows/upload-conan-deps.yml
@@ -68,7 +68,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Prepare runner
-        uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f
+        uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c
         with:
           enable_ccache: false
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4cbf4c1dd0..7bac4d1140 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -51,12 +51,12 @@ repos:
         exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/
 
   - repo: https://github.com/BlankSpruce/gersemi-pre-commit
-    rev: faadd6a9d852369ca94f4d15b2404c967ba8cb01 # frozen: 0.27.6
+    rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7
     hooks:
       - id: gersemi
 
   - repo: https://github.com/rbubley/mirrors-prettier
-    rev: 515f543f5718ebfd6ce22e16708bb32c68ff96e1 # frozen: v3.8.3
+    rev: 39e2973981e6d2f9b6c543b0086a2d2393abdc89 # frozen: v3.9.4
     hooks:
       - id: prettier
         args: [--end-of-line=auto]
@@ -86,7 +86,7 @@ repos:
         files: \.md$
 
   - repo: https://github.com/streetsidesoftware/cspell-cli
-    rev: 4643f154907327ee0a2c7038f0296e0dd77d9776 # frozen: v10.0.0
+    rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1
     hooks:
       - id: cspell # Spell check changed files
         exclude: |
diff --git a/docs/0001-negative-unl/README.md b/docs/0001-negative-unl/README.md
index dd5f9af2ae..0bc65cd860 100644
--- a/docs/0001-negative-unl/README.md
+++ b/docs/0001-negative-unl/README.md
@@ -288,7 +288,7 @@ components with non-trivial changes are colored green.
     validated.
 
 ![Sequence diagram](./negativeUNL_highLevel_sequence.png?raw=true "Negative UNL
- Changes")
+Changes")
 
 ## Roads Not Taken
 

From 6d0b758a12eeb24b3c980d4673be3673040fdc63 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Wed, 1 Jul 2026 14:28:41 +0100
Subject: [PATCH 031/100] build: Don't reuse binaries between different C++
 versions (#7681)

---
 conan/profiles/default    | 12 +++++++-----
 conan/profiles/sanitizers | 10 +++++-----
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/conan/profiles/default b/conan/profiles/default
index e0a88ebca1..ae6e23c3c3 100644
--- a/conan/profiles/default
+++ b/conan/profiles/default
@@ -10,16 +10,18 @@
 os={{ os }}
 arch={{ arch }}
 build_type=Debug
-compiler={{compiler}}
+compiler={{ compiler }}
 compiler.version={{ compiler_version }}
 compiler.cppstd=23
 {% if os == "Windows" %}
 compiler.runtime=static
 {% else %}
-compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}}
+compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }}
 {% endif %}
 
 [conf]
-{% if compiler == "gcc" and compiler_version < 13 %}
-tools.build:cxxflags+=['-Wno-restrict']
-{% endif %}
+{# By default, Conan tries to reuse binaries built with different cppstd versions. #}
+{# We want to avoid that to improve reproduceability, so we add the cppstd version to the package ID. #}
+{# More info: https://docs.conan.io/2/reference/extensions/binary_compatibility.html #}
+user.package:cppstd_version=23
+tools.info.package_id:confs+=["user.package:cppstd_version"]
diff --git a/conan/profiles/sanitizers b/conan/profiles/sanitizers
index 083807ea9e..09e6aef02b 100644
--- a/conan/profiles/sanitizers
+++ b/conan/profiles/sanitizers
@@ -87,15 +87,15 @@ include(default)
 {% endif %}
 
 [conf]
-tools.build:defines+={{defines}}
-tools.build:cxxflags+={{sanitizer_compiler_flags}}
-tools.build:sharedlinkflags+={{sanitizer_linker_flags}}
-tools.build:exelinkflags+={{sanitizer_linker_flags}}
+tools.build:defines+={{ defines }}
+tools.build:cxxflags+={{ sanitizer_compiler_flags }}
+tools.build:sharedlinkflags+={{ sanitizer_linker_flags }}
+tools.build:exelinkflags+={{ sanitizer_linker_flags }}
 
 tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"]
 
 # &: means "apply only to the consumer/root package"
-&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags | join(' ')}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags | join(' ')}}"}
+&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{ sanitizers }}", "SANITIZERS_COMPILER_FLAGS": "{{ sanitizer_compiler_flags | join(' ') }}", "SANITIZERS_LINKER_FLAGS": "{{ sanitizer_linker_flags | join(' ') }}"}
 
 [options]
 {% if enable_asan %}

From ba739c94ce184933e728295527131242317acb9b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 1 Jul 2026 09:29:06 -0400
Subject: [PATCH 032/100] ci: [DEPENDABOT] bump actions/setup-python from 6.2.0
 to 6.3.0 (#7657)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/reusable-package.yml         | 2 +-
 .github/workflows/reusable-strategy-matrix.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml
index 249e807592..6feecbfb75 100644
--- a/.github/workflows/reusable-package.yml
+++ b/.github/workflows/reusable-package.yml
@@ -30,7 +30,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Set up Python
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+        uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
         with:
           python-version: "3.13"
 
diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml
index c1a1c1a78b..690aa3d423 100644
--- a/.github/workflows/reusable-strategy-matrix.yml
+++ b/.github/workflows/reusable-strategy-matrix.yml
@@ -26,7 +26,7 @@ jobs:
         uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
       - name: Set up Python
-        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+        uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
         with:
           python-version: "3.13"
 

From d1ff948244cb8b3027a74851466e8816f7940a90 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Wed, 1 Jul 2026 14:30:03 +0100
Subject: [PATCH 033/100] test: Add tests for TMProofPathResponse and
 TMReplayDeltaResponse invalid hash/key sizes (#7593)

---
 src/test/app/LedgerReplay_test.cpp | 62 ++++++++++++++++++++++++++++++
 src/test/basics/base_uint_test.cpp | 18 +++++++++
 2 files changed, 80 insertions(+)

diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp
index ed99817f34..da98c03380 100644
--- a/src/test/app/LedgerReplay_test.cpp
+++ b/src/test/app/LedgerReplay_test.cpp
@@ -982,6 +982,46 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
             BEAST_EXPECT(!reply->has_error());
             BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply));
 
+            {
+                // bad reply: invalid hash/key sizes
+                {
+                    // reply with undersized ledgerhash (31 bytes)
+                    auto bad = std::make_shared(*reply);
+                    bad->set_ledgerhash(std::string(31, '\x01'));
+                    BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
+                }
+                {
+                    // reply with oversized ledgerhash (33 bytes)
+                    auto bad = std::make_shared(*reply);
+                    bad->set_ledgerhash(std::string(33, '\x01'));
+                    BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
+                }
+                {
+                    // reply with empty ledgerhash
+                    auto bad = std::make_shared(*reply);
+                    bad->set_ledgerhash(std::string());
+                    BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
+                }
+                {
+                    // reply with undersized key (31 bytes)
+                    auto bad = std::make_shared(*reply);
+                    bad->set_key(std::string(31, '\x01'));
+                    BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
+                }
+                {
+                    // reply with oversized key (33 bytes)
+                    auto bad = std::make_shared(*reply);
+                    bad->set_key(std::string(33, '\x01'));
+                    BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
+                }
+                {
+                    // reply with empty key
+                    auto bad = std::make_shared(*reply);
+                    bad->set_key(std::string());
+                    BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
+                }
+            }
+
             {
                 // bad reply
                 // bad header
@@ -1031,6 +1071,28 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
             BEAST_EXPECT(!reply->has_error());
             BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply));
 
+            {
+                // bad reply: invalid hash sizes
+                {
+                    // reply with undersized ledgerhash (31 bytes)
+                    auto bad = std::make_shared(*reply);
+                    bad->set_ledgerhash(std::string(31, '\x01'));
+                    BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
+                }
+                {
+                    // reply with oversized ledgerhash (33 bytes)
+                    auto bad = std::make_shared(*reply);
+                    bad->set_ledgerhash(std::string(33, '\x01'));
+                    BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
+                }
+                {
+                    // reply with empty ledgerhash
+                    auto bad = std::make_shared(*reply);
+                    bad->set_ledgerhash(std::string());
+                    BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
+                }
+            }
+
             {
                 // bad reply
                 // bad header
diff --git a/src/test/basics/base_uint_test.cpp b/src/test/basics/base_uint_test.cpp
index 5816b4eb59..c6572a0028 100644
--- a/src/test/basics/base_uint_test.cpp
+++ b/src/test/basics/base_uint_test.cpp
@@ -117,11 +117,29 @@ struct base_uint_test : beast::unit_test::Suite
         }
     }
 
+    void
+    testFromRawSizeMismatch()
+    {
+        testcase("base_uint: fromRaw size mismatch");
+
+        // Container larger than the base_uint (16 bytes vs 12 bytes for test96).
+        // Only the first 12 bytes are copied; the extra bytes are ignored.
+        {
+            Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+            test96 const result = test96::fromRaw(tooBig);
+            BEAST_EXPECT(to_string(result) == "0102030405060708090A0B0C");
+        }
+    }
+
     void
     run() override
     {
         testcase("base_uint: general purpose tests");
 
+#ifdef NDEBUG
+        testFromRawSizeMismatch();
+#endif
+
         static_assert(!std::is_constructible_v>);
         static_assert(!std::is_assignable_v>);
 

From 0d149ba5b621d7ae6210c8ed005c3109a4782413 Mon Sep 17 00:00:00 2001
From: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Date: Wed, 1 Jul 2026 17:04:38 +0200
Subject: [PATCH 034/100] fix: Disable AMM creation with Vault shares (#7666)

---
 include/xrpl/ledger/helpers/MPTokenHelpers.h |   9 +
 include/xrpl/ledger/helpers/TokenHelpers.h   |  32 +-
 src/libxrpl/ledger/helpers/TokenHelpers.cpp  |  61 +-
 src/libxrpl/tx/invariants/MPTInvariant.cpp   |  25 +-
 src/libxrpl/tx/transactors/dex/AMMCreate.cpp |  17 +
 src/test/app/AMMMPT_test.cpp                 | 176 +-----
 src/test/app/Invariants_test.cpp             | 217 ++-----
 src/test/app/Vault_test.cpp                  | 619 ++++++++++---------
 8 files changed, 495 insertions(+), 661 deletions(-)

diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h
index c709badab8..b7f87337cf 100644
--- a/include/xrpl/ledger/helpers/MPTokenHelpers.h
+++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h
@@ -23,6 +23,15 @@ namespace xrpl {
 [[nodiscard]] bool
 isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
 
+/** Returns true if @p account's MPToken for @p mptIssue carries the
+ *  individual-lock flag (lsfMPTLocked).
+ *
+ *  @warning This checks only the raw per-holder lock bit.  It does **not**
+ *  perform the transitive vault pseudo-account check: if @p mptIssue is a
+ *  vault share whose underlying asset is frozen, this function returns false.
+ *  Call @ref isFrozen instead when determining whether an account may send or
+ *  receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and
+ *  isVaultPseudoAccountFrozen into a single complete check. */
 [[nodiscard]] bool
 isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
 
diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h
index 0c9871cd76..c8a67f776a 100644
--- a/include/xrpl/ledger/helpers/TokenHelpers.h
+++ b/include/xrpl/ledger/helpers/TokenHelpers.h
@@ -144,19 +144,18 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
  *
  * Otherwise checks, in order:
  *   1. If the asset is globally frozen the remaining checks are redundant.
- *   2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its
- * underlying asset.
- *   3. The pseudo-account's trustline / MPToken must not be frozen for sending.
- *   4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent
- * recovering one's own funds.
- *   5. The destination must not be deep-frozen (cannot receive under any circumstance).
+ *   2. The pseudo-account's trustline / MPToken must not be individually frozen for sending.
+ *   3. The submitter's trustline / MPToken must not be individually frozen. Skipped when
+ * submitter == dst (self-withdrawal) so a regular freeze does not prevent recovering one's own
+ * funds. (Enforced as defensive code; no current caller exercises a frozen submitter ≠ dst.)
+ *   4. The destination must not be deep-frozen.
  *
- * For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep
- * freeze does.  For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
+ * For IOUs a regular individual freeze on the submitter does NOT block self-withdrawal; only deep
+ * freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
  * blocked.
  *
  * @param view          Ledger view to read freeze state from.
- * @param srcAcct       Pseudo-account the funds are withdrawn from (sender).
+ * @param pseudoAcct       Pseudo-account the funds are withdrawn from (sender).
  * @param submitterAcct Account that submitted the withdrawal transaction.
  * @param dstAcct       Account receiving the withdrawn funds.
  * @param asset         Asset being withdrawn.
@@ -166,7 +165,7 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
 [[nodiscard]] TER
 checkWithdrawFreeze(
     ReadView const& view,
-    AccountID const& srcAcct,
+    AccountID const& pseudoAcct,
     AccountID const& submitterAcct,
     AccountID const& dstAcct,
     Asset const& asset);
@@ -175,20 +174,17 @@ checkWithdrawFreeze(
  * Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM,
  * LoanBroker).
  *
- *
  * Checks, in order:
  *   1. If the asset is globally frozen the remaining checks are redundant.
- *   2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its
- * underlying asset (returns tecLOCKED).
- *   3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer,
- * since the issuer can always send its own asset.
- *   4. The pseudo-account must not be individually frozen for the asset.  Unlike regular accounts,
+ *   2. The depositor must not be individually frozen for the asset. Skipped when srcAcct is the
+ * asset issuer, since the issuer can always send its own asset.
+ *   3. The pseudo-account must not be individually frozen for the asset.  Unlike regular accounts,
  * pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds
  * could not later be withdrawn.
  *
  * @param view    Ledger view to read freeze state from.
  * @param srcAcct Depositor sending the funds.
- * @param dstAcct Pseudo-account receiving the deposit.
+ * @param pseudoAcct Pseudo-account receiving the deposit.
  * @param asset   Asset being deposited.
  * @return tesSUCCESS if the deposit is permitted, otherwise a freeze result
  *         (tecFROZEN for IOUs, tecLOCKED for MPTs).
@@ -197,7 +193,7 @@ checkWithdrawFreeze(
 checkDepositFreeze(
     ReadView const& view,
     AccountID const& srcAcct,
-    AccountID const& dstAcct,
+    AccountID const& pseudoAcct,
     Asset const& asset);
 
 //------------------------------------------------------------------------------
diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
index 7988e24a56..0ab97a4b60 100644
--- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
@@ -160,19 +160,24 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
 [[nodiscard]] TER
 checkWithdrawFreeze(
     ReadView const& view,
-    AccountID const& srcAcct,
+    AccountID const& pseudoAcct,
     AccountID const& submitterAcct,
     AccountID const& dstAcct,
     Asset const& asset)
 {
     XRPL_ASSERT(
-        isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source is a pseudo-account");
+        isPseudoAccount(view, pseudoAcct),
+        "xrpl::checkWithdrawFreeze : source is a pseudo-account");
     XRPL_ASSERT(
         !isPseudoAccount(view, submitterAcct),
         "xrpl::checkWithdrawFreeze : submitter is not a pseudo-account");
     XRPL_ASSERT(
         !isPseudoAccount(view, dstAcct),
         "xrpl::checkWithdrawFreeze : destination is not a pseudo-account");
+    // The asset being withdrawn must not be issued by a pseudo-account
+    XRPL_ASSERT(
+        !isPseudoAccount(view, asset.getIssuer()),
+        "xrpl::checkWithdrawFreeze : asset issuer cannot be a pseudo-account");
 
     // Funds can always be sent to the issuer
     if (dstAcct == asset.getIssuer())
@@ -182,16 +187,9 @@ checkWithdrawFreeze(
     if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret))
         return ret;
 
-    // Special case for shares - check if the shares (and the transitive asset) is not frozen
-    if (asset.holds() &&
-        isVaultPseudoAccountFrozen(view, srcAcct, asset.get(), 0))
-    {
-        return tecLOCKED;
-    }
-
     // The transfer is from Submitter to Destination via Source (pseudo-account)
     // Both Source and Submitter must not be frozen to allow sending funds
-    if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
+    if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret))
         return ret;
 
     // Check submitter's individual freeze only when Submitter != Destination (a regular freeze
@@ -203,33 +201,42 @@ checkWithdrawFreeze(
     }
 
     // The destination account must not be deep frozen to receive the funds
-    return checkDeepFrozen(view, dstAcct, asset);
+    if (auto const ret = checkDeepFrozen(view, dstAcct, asset); !isTesSuccess(ret))
+        return ret;
+
+    if (asset.holds() &&
+        isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0))
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::checkWithdrawFreeze : pseudo-account backed object holds shares");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
+
+    return tesSUCCESS;
 }
 
 [[nodiscard]] TER
 checkDepositFreeze(
     ReadView const& view,
     AccountID const& srcAcct,
-    AccountID const& dstAcct,
+    AccountID const& pseudoAcct,
     Asset const& asset)
 {
     XRPL_ASSERT(
-        isPseudoAccount(view, dstAcct),
+        isPseudoAccount(view, pseudoAcct),
         "xrpl::checkDepositFreeze : destination is a pseudo-account");
     XRPL_ASSERT(
         !isPseudoAccount(view, srcAcct),
         "xrpl::checkDepositFreeze : source is not a pseudo-account");
+    // The asset being deposited must not be issued by a pseudo-account
+    XRPL_ASSERT(
+        !isPseudoAccount(view, asset.getIssuer()),
+        "xrpl::checkDepositFreeze : asset issuer cannot be a pseudo-account");
 
     if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret))
         return ret;
 
-    // Special case for shares - check if the shares and the transitive asset is not frozen
-    if (asset.holds() &&
-        isVaultPseudoAccountFrozen(view, dstAcct, asset.get(), 0))
-    {
-        return tecLOCKED;
-    }
-
     if (srcAcct != asset.getIssuer())
     {
         if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret))
@@ -238,7 +245,19 @@ checkDepositFreeze(
 
     // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze
     // because those funds cannot be later withdrawn
-    return checkIndividualFrozen(view, dstAcct, asset);
+    if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret))
+        return ret;
+
+    if (asset.holds() &&
+        isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0))
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::checkDepositFreeze : pseudo-account backed object holds shares");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
+
+    return tesSUCCESS;
 }
 
 //------------------------------------------------------------------------------
diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp
index 26bee4effb..d8f7fcc27d 100644
--- a/src/libxrpl/tx/invariants/MPTInvariant.cpp
+++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp
@@ -6,7 +6,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -837,8 +836,6 @@ ValidMPTTransfer::finalize(
     ReadView const& view,
     beast::Journal const& j)
 {
-    auto const fix330Enabled = view.rules().enabled(fixCleanup3_3_0);
-
     if (hasPrivilege(tx, OverrideFreeze))
         return true;
 
@@ -901,27 +898,9 @@ ValidMPTTransfer::finalize(
 
                 // Check once: if any involved account is frozen, the whole issuance transfer is
                 // considered frozen. Only need to check for frozen if there is a transfer of funds.
-                //
-                // Post-fix330: full isFrozen() applies — vault-share transitive freeze is part of
-                // the freeze semantics for all changed holders.
-                //
-                // Pre-fix330: legacy AMM withdraw only checked individual freeze on the
-                // destination, not the transitive vault freeze.  All other paths (and the AMM
-                // account itself as sender) did apply the full check.
-                MPTIssue const issue{mptID};
-                auto const legacyAccountFrozen = [&] {
-                    if (isGlobalFrozen(view, issue) || isIndividualFrozen(view, account, issue))
-                        return true;
-                    bool const isReceiver =
-                        !value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore;
-                    if (txnType == ttAMM_WITHDRAW && isReceiver)
-                        return false;
-                    return isVaultPseudoAccountFrozen(view, account, issue, 0);
-                };
-                bool const accountFrozen =
-                    fix330Enabled ? isFrozen(view, account, issue) : legacyAccountFrozen();
                 if (!invalidTransfer &&
-                    (accountFrozen || !isAuthorized(view, mptID, account, reqAuth)))
+                    (isFrozen(view, account, MPTIssue{mptID}) ||
+                     !isAuthorized(view, mptID, account, reqAuth)))
                 {
                     invalidTransfer = true;
                 }
diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp
index ef271ba362..66d059c54d 100644
--- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp
@@ -193,6 +193,23 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
                 pseudoAccountAddress(ctx.view, keylet::amm(amount.asset(), amount2.asset()).key);
             accountId == beast::kZero)
             return terADDRESS_COLLISION;
+
+        auto const isMPTIssuerPseudo = [&](Asset const& asset) {
+            if (asset.native())
+                return false;
+
+            if (asset.holds())
+                return false;
+
+            return isPseudoAccount(ctx.view, asset.getIssuer());
+        };
+
+        if (isMPTIssuerPseudo(amount.asset()) || isMPTIssuerPseudo(amount2.asset()))
+        {
+            JLOG(ctx.j.debug()) << "AMM Instance: can't create with vault shares " << amount << " "
+                                << amount2;
+            return tecWRONG_ASSET;
+        }
     }
 
     if (auto const ter = canMPTTradeAndTransfer(ctx.view, amount.asset(), accountID, accountID);
diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp
index 9ff6c65c17..c68270591a 100644
--- a/src/test/app/AMMMPT_test.cpp
+++ b/src/test/app/AMMMPT_test.cpp
@@ -6892,168 +6892,38 @@ private:
     void
     testAMMWithVaultShares()
     {
-        testcase("AMM with vault shares — underlying freeze blocks share withdrawal");
+        testcase("AMM with vault shares not allowed");
         using namespace jtx;
+
         // AMMTestBase::testableAmendments() strips featureSingleAssetVault,
         // but vault shares require it. Use the global jtx set directly.
-        FeatureBitset const all{jtx::testableAmendments()};
+        Env env{*this, envconfig(), jtx::testableAmendments(), nullptr, beast::Severity::Disabled};
 
-        // When alice's underlying asset is individually frozen:
-        //
-        // Deposit (post-fixCleanup3_3_0): checkDepositFreeze checks the AMM
-        //   pseudo-account's underlying, not alice's — deposit is allowed.
-        //   Pre-fix: featureAMMClawback calls isFrozen(alice, share) which
-        //   descends via isVaultPseudoAccountFrozen(alice,...) and finds the
-        //   frozen underlying — deposit is blocked.
-        //
-        // Withdrawal (post-fixCleanup3_3_0): checkWithdrawFreeze ends with
-        //   checkDeepFrozen(alice, share) which calls isFrozen(alice, share)
-        //   and finds the frozen underlying — withdrawal is blocked.
-        //   Pre-fix: the old path only checks the AMM account's MPToken lock,
-        //   which is unset — withdrawal succeeds.
+        env.fund(XRP(100'000), gw_, alice_);
+        env(fset(gw_, asfDefaultRipple));
+        env.close();
 
-        auto runIOU = [&](FeatureBitset const& features) {
-            bool const fix330 = features[fixCleanup3_3_0];
-            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+        PrettyAsset const iou = gw_["IOU"];
+        env.trust(iou(1'000'000), alice_);
+        env(pay(gw_, alice_, iou(10'000)));
+        env.close();
 
-            env.fund(XRP(100'000), gw_, alice_);
-            env(fset(gw_, asfDefaultRipple));
-            env.close();
+        Vault const vault{env};
+        auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou});
+        env(createTx);
+        env.close();
 
-            PrettyAsset const iou = gw_["IOU"];
-            env.trust(iou(1'000'000), alice_);
-            env(pay(gw_, alice_, iou(10'000)));
-            env.close();
+        env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)}));
+        env.close();
 
-            Vault const vault{env};
-            auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou});
-            env(createTx);
-            env.close();
+        auto const vaultSle = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultSle))
+            return;
 
-            // 200 IOU → 200,000,000 vault shares (IOU vault scale = 6)
-            env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)}));
-            env.close();
-
-            auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID);
-            // Use half the shares for the AMM; alice keeps the other half.
-            STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000};
-            // Pool: XRP(100) = 1e8 drops, shares = 1e8 → LP ≈ 1e8
-            AMM amm{env, alice_, XRP(100), shareAmt};
-            env.close();
-
-            // Freeze alice's IOU trustline (individual freeze on underlying).
-            env(trust(gw_, iou(0), alice_, tfSetFreeze));
-            env.close();
-
-            // post-fix330: checkDepositFreeze checks AMM pseudo's underlying
-            //              (not alice's) → deposit is allowed
-            // pre-fix330:  featureAMMClawback path calls isFrozen(alice, share)
-            //              which descends to alice's frozen IOU → tecLOCKED
-            amm.deposit(
-                {.account = alice_,
-                 .asset1In = XRP(1),
-                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))});
-
-            // post-fix330: checkWithdrawFreeze → checkDeepFrozen(alice, share)
-            //              descends to alice's frozen IOU → tecLOCKED
-            // pre-fix330:  the AMM pseudo-account is not authorized for the
-            //              share's underlying (requireAuth recurses share→IOU
-            //              and the AMM holds no IOU trustline), so
-            //              accountHolds(ZeroIfUnauthorized) reports the pool's
-            //              share balance as 0 and the withdrawal math fails →
-            //              tecAMM_FAILED.  Vault shares deposited into an AMM are
-            //              only withdrawable once fixCleanup3_3_0 exempts the
-            //              pseudo-account from the recursive auth check.
-            amm.withdraw(
-                {.account = alice_,
-                 .tokens = 1'000,
-                 .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))});
-
-            env(trust(gw_, iou(0), alice_, tfClearFreeze));
-            env.close();
-
-            // Lifting the freeze lets the deposit through in both cases.  The
-            // withdrawal only succeeds post-fix330; pre-fix330 the share balance
-            // remains inaccessible to the unauthorized pseudo-account, so the
-            // shares stay stuck → tecAMM_FAILED.
-            amm.deposit({.account = alice_, .asset1In = XRP(1)});
-            amm.withdraw(
-                {.account = alice_,
-                 .tokens = 1'000,
-                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))});
-        };
-
-        runIOU(all);
-        runIOU(all - fixCleanup3_3_0);
-
-        auto runMPT = [&](FeatureBitset const& features) {
-            bool const fix330 = features[fixCleanup3_3_0];
-            // Expected freeze failures fire invariant checks that log at Error;
-            // silence them so the test output stays clean.
-            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
-
-            env.fund(XRP(100'000), gw_, alice_);
-            env.close();
-
-            MPTTester mptt{env, gw_, kMptInitNoFund};
-            mptt.create({.flags = kMptDexFlags | tfMPTCanLock});
-            PrettyAsset const mpt = mptt.issuanceID();
-            mptt.authorize({.account = alice_});
-            env(pay(gw_, alice_, mpt(30'000)));
-            env.close();
-
-            Vault const vault{env};
-            auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt});
-            env(createTx);
-            env.close();
-
-            // 20000 MPT → 20000 vault shares (MPT vault scale = 0)
-            env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(20'000)}));
-            env.close();
-
-            auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID);
-            // Use half the shares for the AMM; alice keeps the other half.
-            // Pool: XRP(100) = 1e8 drops, shares = 10000 → LP ≈ 1e6
-            STAmount const shareAmt{MPTIssue{shareMPTID}, 10'000};
-            AMM amm{env, alice_, XRP(100), shareAmt};
-            env.close();
-
-            // Lock alice's underlying MPT (individual lock).
-            mptt.set({.holder = alice_, .flags = tfMPTLock});
-
-            // Same pre/post-fix330 semantics as the IOU case above.
-            amm.deposit(
-                {.account = alice_,
-                 .asset1In = XRP(1),
-                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))});
-
-            // {.tokens = 1'000} → frac = 1000/1e6 = 0.001
-            // XRP out = 1e8 * 0.001 = 1e5 drops, shares out = 10000 * 0.001 = 10
-            // post-fix330: checkWithdrawFreeze sees alice's locked underlying
-            //              MPT via the share → tecLOCKED.
-            // pre-fix330:  the AMM pseudo-account is unauthorized for the
-            //              share's underlying MPT (it holds no underlying
-            //              MPToken), so accountHolds(ZeroIfUnauthorized) zeros
-            //              the pool's share balance and the math fails →
-            //              tecAMM_FAILED.
-            amm.withdraw(
-                {.account = alice_,
-                 .tokens = 1'000,
-                 .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))});
-
-            mptt.set({.holder = alice_, .flags = tfMPTUnlock});
-
-            // Unlocking lets the deposit through; the withdrawal only succeeds
-            // post-fix330 (pre-fix330 the shares remain stuck → tecAMM_FAILED).
-            amm.deposit({.account = alice_, .asset1In = XRP(1)});
-            amm.withdraw(
-                {.account = alice_,
-                 .tokens = 1'000,
-                 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))});
-        };
-
-        runMPT(all);
-        runMPT(all - fixCleanup3_3_0);
+        auto const shareMPTID = vaultSle->at(sfShareMPTID);
+        STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000};
+        AMM const amm{env, alice_, XRP(100), shareAmt, Ter(tecWRONG_ASSET)};
+        env.close();
     }
 
     void
diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp
index 079ff9df88..79fdbb54ed 100644
--- a/src/test/app/Invariants_test.cpp
+++ b/src/test/app/Invariants_test.cpp
@@ -4,7 +4,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -65,6 +64,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -4606,15 +4606,17 @@ class Invariants_test : public beast::unit_test::Suite
             }
         }
 
-        // Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen
-        // on fixCleanup3_3_0.  Pre-amendment, vault-share transfers are allowed
-        // even when the underlying asset is individually frozen for the sender;
-        // post-amendment they are blocked.
+        // Vault-share freeze invariant: isVaultPseudoAccountFrozen descends
+        // through sfReferenceHolding to test the vault's underlying asset for
+        // each changed holder.
         {
             Account const gw{"gw"};
             MPTID shareID{};
 
-            auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+            // Vault setup: a1 and a2 both deposit IOU and hold vault shares.
+            auto const setupVault = [&](Account const& a1,
+                                        Account const& a2,
+                                        Env& env) -> std::tuple {
                 env.fund(XRP(1'000), gw);
                 env.trust(gw["IOU"](10'000), a1);
                 env.trust(gw["IOU"](10'000), a2);
@@ -4623,25 +4625,17 @@ class Invariants_test : public beast::unit_test::Suite
                 env(pay(gw, a2, gw["IOU"](500)));
                 env.close();
 
-                PrettyAsset const iou = gw["IOU"];
                 Vault const vault{env};
-                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou});
+                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
                 env(createTx);
                 env.close();
-                // Both a1 and a2 deposit IOU, each receiving vault shares.
-                env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)}));
-                env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)}));
+                env(vault.deposit(
+                    {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
+                env(vault.deposit(
+                    {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
                 env.close();
 
-                shareID = env.le(vaultKeylet)->at(sfShareMPTID);
-
-                // Freeze a2's IOU trustline from the issuer side.
-                // a2 is the receiver in the simulated AMM withdraw; the
-                // distinction under test is that pre-fix330 the invariant
-                // does not apply the transitive vault freeze to receivers.
-                env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
-                env.close();
-                return true;
+                return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)};
             };
 
             // Simulate a vault-share transfer: a1 sends 10 shares to a2.
@@ -4658,156 +4652,45 @@ class Invariants_test : public beast::unit_test::Suite
                 return true;
             };
 
-            // post-fixCleanup3_3_0: full isFrozen() applies to all holders;
-            // isVaultPseudoAccountFrozen finds a2's underlying IOU frozen →
-            // invalidTransfer → invariant fires.
-            doInvariantCheck(
-                Env{*this, defaultAmendments()},
-                {{"invalid MPToken transfer between holders"}},
-                precheck,
-                XRPAmount{},
-                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                preclose);
+            // Case: vault pseudo-account's IOU trustline is frozen.
+            {
+                auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                    auto [sid, vid] = setupVault(a1, a2, env);
+                    shareID = sid;
+                    env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vid}, tfSetFreeze));
+                    env.close();
+                    return true;
+                };
 
-            // pre-fixCleanup3_3_0: legacy AMM withdraw only checked
-            // checkIndividualFrozen on the destination, not the transitive
-            // vault freeze; a2 as receiver is exempt → invariant passes.
-            doInvariantCheck(
-                Env{*this, defaultAmendments() - fixCleanup3_3_0},
-                {},
-                precheck,
-                XRPAmount{},
-                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
-                {tesSUCCESS, tesSUCCESS},
-                preclose);
-        }
+                doInvariantCheck(
+                    Env{*this, defaultAmendments()},
+                    {{"invalid MPToken transfer between holders"}},
+                    precheck,
+                    XRPAmount{},
+                    STTx{ttPAYMENT, [](STObject&) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    preclose);
+            }
 
-        // Side-specific vault-share AMM_WITHDRAW invariant tests.
-        // Both cases use a real vault (IOU underlying) and a real AMM whose
-        // pool includes vault shares.  precheck simulates an AMM_WITHDRAW by
-        // transferring 10 vault shares from the AMM pseudo-account to a2.
-        {
-            MPTID shareID{};
-            AccountID ammAcctID{};
-            AccountID vaultPseudoID{};
-            Account const gw{"gw"};
+            // Case: receiver's (a2's) IOU trustline is frozen.
+            {
+                auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                    auto [sid, vid] = setupVault(a1, a2, env);
+                    shareID = sid;
+                    env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
+                    env.close();
+                    return true;
+                };
 
-            // Simulate AMM_WITHDRAW: AMM pseudo-account sends 10 vault shares
-            // to a2.  The AMM pseudo is the sender (decreasing balance);
-            // a2 is the receiver (increasing balance).
-            auto const precheck2 =
-                [&](Account const& /*a1*/, Account const& a2, ApplyContext& ac) -> bool {
-                auto sleAMM = ac.view().peek(keylet::mptoken(shareID, ammAcctID));
-                auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
-                if (!sleAMM || !sle2)
-                    return false;
-                (*sleAMM)[sfMPTAmount] -= 10;
-                (*sle2)[sfMPTAmount] += 10;
-                ac.view().update(sleAMM);
-                ac.view().update(sle2);
-                return true;
-            };
-
-            // Shared vault + AMM setup: a1 deposits 500 IOU into a vault and
-            // creates an AMM with XRP + 100 vault shares, giving the AMM
-            // pseudo-account a vault-share MPToken balance.
-            auto const setupVaultAMM = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-                env.fund(XRP(1'000), gw);
-                env(fset(gw, asfDefaultRipple));
-                env.close();
-
-                env.trust(gw["IOU"](10'000), a1);
-                env.trust(gw["IOU"](10'000), a2);
-                env.close();
-                env(pay(gw, a1, gw["IOU"](1'000)));
-                env(pay(gw, a2, gw["IOU"](500)));
-                env.close();
-
-                Vault const vault{env};
-                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
-                env(createTx);
-                env.close();
-
-                env(vault.deposit(
-                    {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](500)}));
-                env(vault.deposit(
-                    {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](200)}));
-                env.close();
-
-                shareID = env.le(vaultKeylet)->at(sfShareMPTID);
-                vaultPseudoID = env.le(vaultKeylet)->at(sfAccount);
-
-                // a1 creates AMM with XRP + 100 vault shares; the AMM
-                // pseudo-account receives an MPToken record for shareID.
-                AMM const amm(env, a1, XRP(100), STAmount{MPTIssue{shareID}, 100});
-                ammAcctID = amm.ammAccount();
-                return true;
-            };
-
-            // Case 1: freeze the vault pseudo-account's IOU trustline.
-            // isVaultPseudoAccountFrozen(ammAcct) calls isAnyFrozen({vaultPseudo,
-            // ammAcct}, IOU); since vaultPseudo is frozen it returns true.  The
-            // AMM sender has a decreasing balance (not a receiver) so it is
-            // never exempt from the check — invariant fires both pre- and
-            // post-fixCleanup3_3_0.
-            auto const preclose3 = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-                if (!setupVaultAMM(a1, a2, env))
-                    return false;
-                env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze));
-                env.close();
-                return true;
-            };
-
-            doInvariantCheck(
-                Env{*this, defaultAmendments()},
-                {{"invalid MPToken transfer between holders"}},
-                precheck2,
-                XRPAmount{},
-                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                preclose3);
-
-            doInvariantCheck(
-                Env{*this, defaultAmendments() - fixCleanup3_3_0},
-                {{"invalid MPToken transfer between holders"}},
-                precheck2,
-                XRPAmount{},
-                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                preclose3);
-
-            // Case 2: freeze a2's (receiver's) IOU trustline.
-            // isVaultPseudoAccountFrozen(a2) → isAnyFrozen({vaultPseudo, a2},
-            // IOU) → true.  The AMM sender's check passes (vaultPseudo and
-            // ammAcct are not frozen).  Pre-fix330: receiver is exempt from
-            // isVaultPseudoAccountFrozen in ttAMM_WITHDRAW → passes.
-            // Post-fix330: full isFrozen() applied to a2 → fires.
-            auto const preclose4 = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-                if (!setupVaultAMM(a1, a2, env))
-                    return false;
-                env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
-                env.close();
-                return true;
-            };
-
-            doInvariantCheck(
-                Env{*this, defaultAmendments()},
-                {{"invalid MPToken transfer between holders"}},
-                precheck2,
-                XRPAmount{},
-                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                preclose4);
-
-            doInvariantCheck(
-                Env{*this, defaultAmendments() - fixCleanup3_3_0},
-                {},
-                precheck2,
-                XRPAmount{},
-                STTx{ttAMM_WITHDRAW, [](STObject&) {}},
-                {tesSUCCESS, tesSUCCESS},
-                preclose4);
+                doInvariantCheck(
+                    Env{*this, defaultAmendments()},
+                    {{"invalid MPToken transfer between holders"}},
+                    precheck,
+                    XRPAmount{},
+                    STTx{ttPAYMENT, [](STObject&) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    preclose);
+            }
         }
     }
 
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index d9afd93b23..f6574872f9 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -1634,8 +1634,6 @@ class Vault_test : public beast::unit_test::Suite
             env(tx, Ter(tecNO_ENTRY));
         });
 
-        // Freeze/lock tests are in testVaultDepositFreeze/testVaultWithdrawFreeze
-
         testCase([this](
                      Env& env,
                      Account const& issuer,
@@ -2238,11 +2236,6 @@ class Vault_test : public beast::unit_test::Suite
             env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
             env.close();
 
-            // The inherited CanTrade restriction also blocks AMM creation.
-            AMM const ammUnderlyingFail(
-                env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION});
-            AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION});
-
             // Deposit still works before enabling CanTrade.
             env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
             env.close();
@@ -7580,185 +7573,212 @@ class Vault_test : public beast::unit_test::Suite
     }
 
     void
-    testVaultDepositFreeze()
+    testVaultDepositFreezeIOU()
     {
         using namespace test::jtx;
+        testcase("VaultDeposit IOU freeze checks");
 
         Account const issuer{"issuer"};
         Account const owner{"owner"};
+        Env env{*this};
+        Vault vault{env};
 
-        // === IOU ===
-        {
-            testcase("VaultDeposit IOU freeze checks");
-            Env env{*this};
-            Vault vault{env};
+        env.fund(XRP(100'000), issuer, owner);
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1'000'000), owner);
+        env(pay(issuer, owner, asset(100'000)));
+        env.close();
 
-            env.fund(XRP(100'000), issuer, owner);
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env.close();
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1'000'000), owner);
-            env(pay(issuer, owner, asset(100'000)));
-            env.close();
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+        auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
 
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-            auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
+        // Initial deposit so the vault pseudo-account has a trustline
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+        env.close();
 
-            // Initial deposit so the vault pseudo-account has a trustline
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
 
-            auto runTests = [&]() {
-                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
-
-                // Global freeze
+            // Global freeze
+            {
+                testcase("VaultDeposit IOU global freeze");
                 env(fset(issuer, asfGlobalFreeze));
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
                     Ter(tecFROZEN));
                 env(fclear(issuer, asfGlobalFreeze));
+            }
 
-                // Depositor regular freeze
+            // Depositor freeze
+            {
+                testcase("VaultDeposit IOU depositor freeze");
                 env(trust(issuer, asset(0), owner, tfSetFreeze));
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
                     Ter(tecFROZEN));
                 env(trust(issuer, asset(0), owner, tfClearFreeze));
+            }
 
-                // Depositor deep freeze
+            // Depositor deep freeze
+            {
+                testcase("VaultDeposit IOU depositor deep freeze");
                 env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
                     Ter(tecFROZEN));
                 env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
+            }
 
-                // Vault-account regular freeze
-                // Post-fix: checkDepositFreeze catches it → tecFROZEN
-                // Pre-fix: not checked directly, but the transitive share
-                //          check triggers → tecLOCKED
-                {
-                    auto trustSet = [&]() {
-                        json::Value jv;
-                        jv[jss::Account] = issuer.human();
-                        {
-                            auto& ja = jv[jss::LimitAmount] =
-                                asset(0).value().getJson(JsonOptions::Values::None);
-                            ja[jss::issuer] = toBase58(vaultAcct.id());
-                        }
-                        jv[jss::TransactionType] = jss::TrustSet;
-                        return jv;
-                    }();
+            // Vault-account freeze
+            // Post-fix: checkDepositFreeze catches it → tecFROZEN
+            // Pre-fix: not checked directly, but the transitive share
+            //          check triggers → tecLOCKED
+            {
+                testcase("VaultDeposit IOU pseudo-account freeze");
+                auto trustSet = [&]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
+                    {
+                        auto& ja = jv[jss::LimitAmount] =
+                            asset(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(vaultAcct.id());
+                    }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    return jv;
+                }();
 
-                    trustSet[jss::Flags] = tfSetFreeze;
-                    env(trustSet);
-                    env.close();
+                trustSet[jss::Flags] = tfSetFreeze;
+                env(trustSet);
+                env.close();
 
-                    TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
-                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                        Ter(expected));
+                TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(expected));
 
-                    trustSet[jss::Flags] = tfClearFreeze;
-                    env(trustSet);
-                    env.close();
-                }
+                trustSet[jss::Flags] = tfClearFreeze;
+                env(trustSet);
+                env.close();
+            }
 
-                // Vault-account deep freeze
-                {
-                    auto trustSet = [&]() {
-                        json::Value jv;
-                        jv[jss::Account] = issuer.human();
-                        {
-                            auto& ja = jv[jss::LimitAmount] =
-                                asset(0).value().getJson(JsonOptions::Values::None);
-                            ja[jss::issuer] = toBase58(vaultAcct.id());
-                        }
-                        jv[jss::TransactionType] = jss::TrustSet;
-                        return jv;
-                    }();
+            // Vault-account deep freeze
+            {
+                testcase("VaultDeposit IOU pseudo-account deep freeze");
+                auto trustSet = [&]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
+                    {
+                        auto& ja = jv[jss::LimitAmount] =
+                            asset(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(vaultAcct.id());
+                    }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    return jv;
+                }();
 
-                    trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze;
-                    env(trustSet);
-                    env.close();
+                trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze;
+                env(trustSet);
+                env.close();
 
-                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                        Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
 
-                    trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze;
-                    env(trustSet);
-                    env.close();
-                }
+                trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze;
+                env(trustSet);
+                env.close();
+            }
 
-                // Clawback works while frozen
+            // Clawback works while frozen
+            {
+                testcase("VaultDeposit IOU freeze clawback unaffected");
                 env(fset(issuer, asfGlobalFreeze));
                 env(vault.clawback(
                     {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
                 env(fclear(issuer, asfGlobalFreeze));
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
                 env.close();
-            };
+            }
+        };
 
-            runTests();
-            env.disableFeature(fixCleanup3_3_0);
-            runTests();
-            env.enableFeature(fixCleanup3_3_0);
-        }
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
 
-        // === MPT ===
-        {
-            testcase("VaultDeposit MPT lock checks");
-            Env env{*this};
-            Vault vault{env};
+    void
+    testVaultDepositFreezeMPT()
+    {
+        using namespace test::jtx;
+        testcase("VaultDeposit MPT lock checks");
 
-            env.fund(XRP(100'000), issuer, owner);
-            env.close();
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Env env{*this};
+        Vault vault{env};
 
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create(
-                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
-            PrettyAsset const mpt{mptt.issuanceID()};
+        env.fund(XRP(100'000), issuer, owner);
+        env.close();
 
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = issuer, .holder = owner});
-            env.close();
-            env(pay(issuer, owner, mpt(100'000)));
-            env.close();
+        MPTTester mptt{env, issuer, kMptInitNoFund};
+        mptt.create(
+            {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+        PrettyAsset const mpt{mptt.issuanceID()};
 
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
-            env(tx);
-            env.close();
-            auto const vaultAcctID = env.le(keylet)->at(sfAccount);
-            Account const vaultAcct("vault", vaultAcctID);
+        mptt.authorize({.account = owner});
+        mptt.authorize({.account = issuer, .holder = owner});
+        env.close();
+        env(pay(issuer, owner, mpt(100'000)));
+        env.close();
 
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
-            env.close();
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
+        env(tx);
+        env.close();
+        auto const vaultAcctID = env.le(keylet)->at(sfAccount);
+        Account const vaultAcct("vault", vaultAcctID);
 
-            // For MPT isDeepFrozen == isFrozen, so all locks block in
-            // both pre- and post-fix.
-            auto runTests = [&]() {
-                // Global lock
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
+        env.close();
+
+        // For MPT isDeepFrozen == isFrozen, so all locks block in
+        // both pre- and post-fix.
+        auto runTests = [&]() {
+            // Global lock
+            {
+                testcase("VaultDeposit MPT global lock");
                 mptt.set({.flags = tfMPTLock});
                 env.close();
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
                     Ter(tecLOCKED));
                 mptt.set({.flags = tfMPTUnlock});
                 env.close();
+            }
 
-                // Depositor individual lock
+            // Depositor individual lock
+            {
+                testcase("VaultDeposit MPT depositor lock");
                 mptt.set({.holder = owner, .flags = tfMPTLock});
                 env.close();
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
                     Ter(tecLOCKED));
                 mptt.set({.holder = owner, .flags = tfMPTUnlock});
                 env.close();
+            }
 
-                // Vault pseudo-account individual lock
+            // Vault pseudo-account individual lock
+            {
+                testcase("VaultDeposit MPT pseudo-account lock");
                 mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
                 env.close();
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
                     Ter(tecLOCKED));
                 mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
                 env.close();
+            }
 
-                // Clawback works while locked
+            // Clawback works while locked
+            {
+                testcase("VaultDeposit MPT lock clawback unaffected");
                 mptt.set({.flags = tfMPTLock});
                 env.close();
                 env(vault.clawback(
@@ -7767,16 +7787,16 @@ class Vault_test : public beast::unit_test::Suite
                 env.close();
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
                 env.close();
-            };
+            }
+        };
 
-            runTests();
-            env.disableFeature(fixCleanup3_3_0);
-            runTests();
-            env.enableFeature(fixCleanup3_3_0);
-        }
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
     }
 
-    // Focused demonstration: a depositor under a regular individual IOU freeze
+    // Focused demonstration: a depositor under an individual IOU freeze
     // can still withdraw to themselves (self-withdrawal), but is blocked from
     // withdrawing to a third party.
     //
@@ -7818,7 +7838,7 @@ class Vault_test : public beast::unit_test::Suite
         auto runTests = [&]() {
             auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
 
-            // Set a regular individual freeze on the owner's IOU trustline.
+            // Set an individual freeze on the owner's IOU trustline.
             env(trust(issuer, asset(0), owner, tfSetFreeze));
             env.close();
 
@@ -7850,110 +7870,111 @@ class Vault_test : public beast::unit_test::Suite
     }
 
     void
-    testVaultWithdrawFreeze()
+    testVaultWithdrawFreezeIOU()
     {
         using namespace test::jtx;
+        testcase("VaultWithdraw IOU freeze checks");
 
         Account const issuer{"issuer"};
         Account const owner{"owner"};
+        Env env{*this};
+        Vault const vault{env};
 
-        // === IOU ===
-        {
-            testcase("VaultWithdraw IOU freeze checks");
-            Env env{*this};
-            Vault vault{env};
+        env.fund(XRP(100'000), issuer, owner);
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1'000'000), owner);
+        env(pay(issuer, owner, asset(100'000)));
+        env.close();
 
-            env.fund(XRP(100'000), issuer, owner);
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env.close();
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1'000'000), owner);
-            env(pay(issuer, owner, asset(100'000)));
-            env.close();
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+        auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
 
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-            auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+        env.close();
 
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
+        Account const charlie{"charlie"};
+        env.fund(XRP(10'000), charlie);
+        env.trust(asset(1'000'000), charlie);
+        env.close();
 
-            Account const charlie{"charlie"};
-            env.fund(XRP(10'000), charlie);
-            env.trust(asset(1'000'000), charlie);
-            env.close();
-
-            auto runTests = [&]() {
-                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
-                // Post-fix: submitter freeze blocks withdraw to 3rd party
-                // Pre-fix: submitter's IOU freeze not checked, but
-                //          checkFrozen(depositor, share) may trigger tecLOCKED
-                TER const submitterTo3rd = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
-
-                // Global freeze → self-withdraw
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+            // Global freeze → self-withdraw
+            {
+                testcase("VaultWithdraw IOU global freeze");
                 env(fset(issuer, asfGlobalFreeze));
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
                     Ter(tecFROZEN));
                 // Global freeze → withdraw to 3rd party
-                {
-                    auto withdrawToCharlie =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                    withdrawToCharlie[sfDestination] = charlie.human();
-                    env(withdrawToCharlie, Ter(tecFROZEN));
-                }
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                env(withdrawToCharlie, Ter(tecFROZEN));
+
                 env(fclear(issuer, asfGlobalFreeze));
+            }
 
-                // Vault-account regular freeze
-                {
-                    auto trustSet = [&]() {
-                        json::Value jv;
-                        jv[jss::Account] = issuer.human();
-                        {
-                            auto& ja = jv[jss::LimitAmount] =
-                                asset(0).value().getJson(JsonOptions::Values::None);
-                            ja[jss::issuer] = toBase58(vaultAcct.id());
-                        }
-                        jv[jss::TransactionType] = jss::TrustSet;
-                        return jv;
-                    }();
-
-                    trustSet[jss::Flags] = tfSetFreeze;
-                    env(trustSet);
-                    env.close();
-
-                    TER const vaultAcctFreeze = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
-
-                    // Self-withdraw
-                    env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                        Ter(vaultAcctFreeze));
-                    // Withdraw to 3rd party
+            // Vault-account freeze
+            {
+                testcase("VaultWithdraw IOU pseudo-account freeze");
+                auto trustSet = [&]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
                     {
-                        auto withdrawToCharlie = vault.withdraw(
-                            {.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                        withdrawToCharlie[sfDestination] = charlie.human();
-                        env(withdrawToCharlie, Ter(vaultAcctFreeze));
+                        auto& ja = jv[jss::LimitAmount] =
+                            asset(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(vaultAcct.id());
                     }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    return jv;
+                }();
 
-                    trustSet[jss::Flags] = tfClearFreeze;
-                    env(trustSet);
-                    env.close();
-                }
+                trustSet[jss::Flags] = tfSetFreeze;
+                env(trustSet);
+                env.close();
 
-                // Depositor regular freeze → self-withdraw
+                TER const terExpected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+
+                // Self-withdraw
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(terExpected));
+                // Withdraw to 3rd party
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                env(withdrawToCharlie, Ter(terExpected));
+
+                trustSet[jss::Flags] = tfClearFreeze;
+                env(trustSet);
+                env.close();
+            }
+
+            // Depositor freeze, self-withdraw
+            {
+                testcase("VaultWithdraw IOU self-withdraw freeze check");
                 env(trust(issuer, asset(0), owner, tfSetFreeze));
+
                 // Post-fix: self-withdraw allowed (submitter==dst skip)
                 // Pre-fix: isFrozen(depositor, iou) catches it
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
                     Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
 
-                // Depositor regular freeze → withdraw to 3rd party
-                {
-                    auto withdrawTo3rd =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                    withdrawTo3rd[sfDestination] = charlie.human();
-                    env(withdrawTo3rd, Ter(submitterTo3rd));
-                }
+                // Depositor freeze withdraw to 3rd party
+                auto withdrawTo3rd =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawTo3rd[sfDestination] = charlie.human();
+
+                // Post-fix: submitter freeze blocks withdraw to 3rd party
+                // Pre-fix: submitter's IOU freeze not checked, but checkFrozen(depositor,
+                // share) triggers tecLOCKED
+                env(withdrawTo3rd, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+
                 env(trust(issuer, asset(0), owner, tfClearFreeze));
                 // Replenish what was withdrawn
                 if (fix330Enabled)
@@ -7961,104 +7982,131 @@ class Vault_test : public beast::unit_test::Suite
                     env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
                 }
                 env.close();
+            }
 
-                // Depositor deep freeze → self-withdraw blocked
+            // Depositor deep freeze → self-withdraw blocked
+            {
+                testcase("VaultWithdraw IOU depositor deep freeze");
                 env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
-                // TODO: branches are identical - confirm the intended pre/post-fix330
-                // expectations and replace with the correct values (one branch may be wrong).
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    // NOLINTNEXTLINE(bugprone-branch-clone)
-                    Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN)));
-                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
 
-                // Destination regular freeze → withdraw to 3rd party
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+
+                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
+            }
+
+            // Destination freeze → withdraw to 3rd party
+            {
+                testcase("VaultWithdraw IOU freeze withdraw to 3rd party");
+
                 env(trust(issuer, asset(0), charlie, tfSetFreeze));
+
                 // Self-withdraw unaffected by charlie's freeze
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-                {
-                    auto withdrawToCharlie =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                    withdrawToCharlie[sfDestination] = charlie.human();
-                    // Post-fix: regular freeze on dst allowed
-                    // Pre-fix: checkFrozen(dst, iou) catches it
-                    env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
-                }
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+
+                // Post-fix: freeze on dst allowed
+                // Pre-fix: checkFrozen(dst, iou) catches it
+                env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+
                 env(trust(issuer, asset(0), charlie, tfClearFreeze));
+
                 // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded
                 env(vault.deposit(
                     {.depositor = owner,
                      .id = keylet.key,
                      .amount = asset(fix330Enabled ? 2 : 1)}));
                 env.close();
+            }
+
+            // Destination deep freeze → withdraw to 3rd party blocked
+            {
+                testcase("VaultWithdraw IOU deep freeze withdraw to 3rd party");
 
-                // Destination deep freeze → withdraw to 3rd party blocked
                 env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze));
-                {
-                    auto withdrawToCharlie =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                    withdrawToCharlie[sfDestination] = charlie.human();
-                    env(withdrawToCharlie, Ter(tecFROZEN));
-                }
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                env(withdrawToCharlie, Ter(tecFROZEN));
+
                 // Destination deep freeze → self-withdraw unaffected
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+
                 env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze));
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
                 env.close();
+            }
 
-                // Clawback works while frozen
+            // Clawback works while frozen
+            {
+                testcase("VaultWithdraw IOU freeze clawback unaffected");
                 env(fset(issuer, asfGlobalFreeze));
+
                 env(vault.clawback(
                     {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
+
                 env(fclear(issuer, asfGlobalFreeze));
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
                 env.close();
-            };
+            }
+        };
 
-            runTests();
-            env.disableFeature(fixCleanup3_3_0);
-            runTests();
-            env.enableFeature(fixCleanup3_3_0);
-        }
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
 
-        // === MPT ===
-        {
-            testcase("VaultWithdraw MPT lock checks");
-            Env env{*this};
-            Vault vault{env};
+    void
+    testVaultWithdrawFreezeMPT()
+    {
+        using namespace test::jtx;
+        testcase("VaultWithdraw MPT lock checks");
 
-            env.fund(XRP(100'000), issuer, owner);
-            env.close();
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Env env{*this};
+        Vault vault{env};
 
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create(
-                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
-            PrettyAsset const mpt{mptt.issuanceID()};
+        env.fund(XRP(100'000), issuer, owner);
+        env.close();
 
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = issuer, .holder = owner});
-            env.close();
-            env(pay(issuer, owner, mpt(100'000)));
-            env.close();
+        MPTTester mptt{env, issuer, kMptInitNoFund};
+        mptt.create(
+            {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+        PrettyAsset const mpt{mptt.issuanceID()};
 
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
-            env(tx);
-            env.close();
-            Account const vaultAcct("vault", env.le(keylet)->at(sfAccount));
+        mptt.authorize({.account = owner});
+        mptt.authorize({.account = issuer, .holder = owner});
+        env.close();
+        env(pay(issuer, owner, mpt(100'000)));
+        env.close();
 
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
-            env.close();
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
+        env(tx);
+        env.close();
+        Account const vaultAcct("vault", env.le(keylet)->at(sfAccount));
 
-            Account const charlie{"charlie"};
-            env.fund(XRP(10'000), charlie);
-            env.close();
-            mptt.authorize({.account = charlie});
-            mptt.authorize({.account = issuer, .holder = charlie});
-            env.close();
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
+        env.close();
 
-            auto runTests = [&]() {
-                auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+        Account const charlie{"charlie"};
+        env.fund(XRP(10'000), charlie);
+        env.close();
+        mptt.authorize({.account = charlie});
+        mptt.authorize({.account = issuer, .holder = charlie});
+        env.close();
 
-                // Global lock
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+            // Global lock
+            {
+                testcase("VaultWithdraw MPT global lock");
                 mptt.set({.flags = tfMPTLock});
                 env.close();
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
@@ -8081,17 +8129,23 @@ class Vault_test : public beast::unit_test::Suite
                     env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
                 }
                 env.close();
+            }
 
-                // Vault pseudo-account individual lock
+            // Vault pseudo-account individual lock
+            {
+                testcase("VaultWithdraw MPT pseudo-account lock");
                 mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
                 env.close();
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
                     Ter(tecLOCKED));
                 mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
                 env.close();
+            }
 
-                // Depositor individual lock → self-withdraw blocked
-                // (isDeepFrozen == isFrozen for MPT)
+            // Depositor individual lock → self-withdraw blocked
+            // (isDeepFrozen == isFrozen for MPT)
+            {
+                testcase("VaultWithdraw MPT depositor lock");
                 mptt.set({.holder = owner, .flags = tfMPTLock});
                 env.close();
                 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
@@ -8120,8 +8174,11 @@ class Vault_test : public beast::unit_test::Suite
                     env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
                 }
                 env.close();
+            }
 
-                // 3rd party destination lock → withdraw to 3rd party blocked
+            // 3rd party destination lock → withdraw to 3rd party blocked
+            {
+                testcase("VaultWithdraw MPT 3rd party destination lock");
                 mptt.set({.holder = charlie, .flags = tfMPTLock});
                 env.close();
                 {
@@ -8136,8 +8193,11 @@ class Vault_test : public beast::unit_test::Suite
                 env.close();
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
                 env.close();
+            }
 
-                // Clawback works while locked
+            // Clawback works while locked
+            {
+                testcase("VaultWithdraw MPT lock clawback unaffected");
                 mptt.set({.flags = tfMPTLock});
                 env.close();
                 env(vault.clawback(
@@ -8146,14 +8206,13 @@ class Vault_test : public beast::unit_test::Suite
                 env.close();
                 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
                 env.close();
-            };
+            }
+        };
 
-            runTests();
-            env.disableFeature(fixCleanup3_3_0);
-
-            runTests();
-            env.enableFeature(fixCleanup3_3_0);
-        }
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
     }
 
 public:
@@ -8198,8 +8257,10 @@ public:
         testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice();
         testWithdrawSoleShareholderLoanRepaymentExit();
 
-        testVaultDepositFreeze();
-        testVaultWithdrawFreeze();
+        testVaultDepositFreezeIOU();
+        testVaultDepositFreezeMPT();
+        testVaultWithdrawFreezeIOU();
+        testVaultWithdrawFreezeMPT();
         testVaultSelfWithdrawWhileFrozen();
 
         testReferenceHolding();

From c53aafa6bfeb8591532c770cfcc59abf5f156a2f Mon Sep 17 00:00:00 2001
From: Timothy Banks 
Date: Wed, 1 Jul 2026 11:36:28 -0400
Subject: [PATCH 035/100] refactor: Retire InnerObjTemplate fix (#7368)

---
 include/xrpl/protocol/detail/features.macro  |  2 +-
 src/libxrpl/ledger/helpers/AMMHelpers.cpp    |  6 ++---
 src/libxrpl/protocol/STObject.cpp            |  6 ++---
 src/libxrpl/tx/transactors/dex/AMMBid.cpp    | 17 ++++----------
 src/libxrpl/tx/transactors/dex/AMMVote.cpp   |  4 +---
 src/test/app/AMM_test.cpp                    | 24 +-------------------
 src/test/jtx/impl/AMM.cpp                    | 10 ++------
 src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp |  4 +---
 8 files changed, 15 insertions(+), 58 deletions(-)

diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index a1911761bb..7e64a49b0c 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -58,7 +58,6 @@ XRPL_FIX    (XChainRewardRounding,       Supported::Yes, VoteBehavior::DefaultNo
 XRPL_FIX    (EmptyDID,                   Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(PriceOracle,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (AMMOverflowOffer,           Supported::Yes, VoteBehavior::DefaultYes)
-XRPL_FIX    (InnerObjTemplate,           Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (FillOrKill,                 Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(DID,                        Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (DisallowIncomingV1,         Supported::Yes, VoteBehavior::DefaultNo)
@@ -101,6 +100,7 @@ XRPL_RETIRE_FIX(1623)
 XRPL_RETIRE_FIX(1781)
 XRPL_RETIRE_FIX(AmendmentMajorityCalc)
 XRPL_RETIRE_FIX(CheckThreading)
+XRPL_RETIRE_FIX(InnerObjTemplate)
 XRPL_RETIRE_FIX(MasterKeyAsRegularKey)
 XRPL_RETIRE_FIX(NonFungibleTokensV1_2)
 XRPL_RETIRE_FIX(NFTokenRemint)
diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp
index 7cd27a1f34..df6d335085 100644
--- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp
@@ -602,9 +602,7 @@ std::uint16_t
 getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account)
 {
     using namespace std::chrono;
-    XRPL_ASSERT(
-        !view.rules().enabled(fixInnerObjTemplate) || ammSle.isFieldPresent(sfAuctionSlot),
-        "xrpl::getTradingFee : auction present");
+    XRPL_ASSERT(ammSle.isFieldPresent(sfAuctionSlot), "xrpl::getTradingFee : auction present");
     if (ammSle.isFieldPresent(sfAuctionSlot))
     {
         auto const& auctionSlot = safeDowncast(ammSle.peekAtField(sfAuctionSlot));
@@ -822,7 +820,7 @@ initializeFeeAuctionVote(
     // AMM creator gets the auction slot for free.
     // AuctionSlot is created on AMMCreate and updated on AMMDeposit
     // when AMM is in an empty state
-    if (rules.enabled(fixInnerObjTemplate) && !ammSle->isFieldPresent(sfAuctionSlot))
+    if (!ammSle->isFieldPresent(sfAuctionSlot))
     {
         STObject auctionSlot = STObject::makeInnerObject(sfAuctionSlot);
         ammSle->set(std::move(auctionSlot));
diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp
index 445da16828..c493ba65af 100644
--- a/src/libxrpl/protocol/STObject.cpp
+++ b/src/libxrpl/protocol/STObject.cpp
@@ -78,12 +78,10 @@ STObject::makeInnerObject(SField const& name)
     // The if is complicated because inner object templates were added in
     // two phases:
     //  1. If there are no available Rules, then always apply the template.
-    //  2. fixInnerObjTemplate added templates to two AMM inner objects.
-    //  3. fixInnerObjTemplate2 added templates to all remaining inner objects.
+    //  2. fixInnerObjTemplate2 added templates to all remaining inner objects.
     std::optional const& rules = getCurrentTransactionRules();
     bool const isAMMObj = name == sfAuctionSlot || name == sfVoteEntry;
-    if (!rules || (rules->enabled(fixInnerObjTemplate) && isAMMObj) ||
-        (rules->enabled(fixInnerObjTemplate2) && !isAMMObj))
+    if (!rules || isAMMObj || rules->enabled(fixInnerObjTemplate2))
     {
         if (SOTemplate const* elements =
                 InnerObjectFormats::getInstance().findSOTemplateBySField(name))
diff --git a/src/libxrpl/tx/transactors/dex/AMMBid.cpp b/src/libxrpl/tx/transactors/dex/AMMBid.cpp
index 83432fa0d0..3454559e82 100644
--- a/src/libxrpl/tx/transactors/dex/AMMBid.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMBid.cpp
@@ -184,18 +184,11 @@ applyBid(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Journa
         return {tecINTERNAL, false};
     STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance];
     auto const lpTokens = ammLPHolds(sb, *ammSle, account, ctx.journal);
-    auto const& rules = ctx.view().rules();
-    if (!rules.enabled(fixInnerObjTemplate))
-    {
-        if (!ammSle->isFieldPresent(sfAuctionSlot))
-            ammSle->makeFieldPresent(sfAuctionSlot);
-    }
-    else
-    {
-        XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyBid : has auction slot");
-        if (!ammSle->isFieldPresent(sfAuctionSlot))
-            return {tecINTERNAL, false};
-    }
+
+    XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyBid : has auction slot");
+    if (!ammSle->isFieldPresent(sfAuctionSlot))
+        return {tecINTERNAL, false};
+
     auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
     auto const current =
         duration_cast(ctx.view().header().parentCloseTime.time_since_epoch()).count();
diff --git a/src/libxrpl/tx/transactors/dex/AMMVote.cpp b/src/libxrpl/tx/transactors/dex/AMMVote.cpp
index 23604d46cc..0f2b721ac4 100644
--- a/src/libxrpl/tx/transactors/dex/AMMVote.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMVote.cpp
@@ -195,9 +195,7 @@ applyVote(ApplyContext& ctx, Sandbox& sb, AccountID const& accountID, beast::Jou
         }
     }
 
-    XRPL_ASSERT(
-        !ctx.view().rules().enabled(fixInnerObjTemplate) || ammSle->isFieldPresent(sfAuctionSlot),
-        "xrpl::applyVote : has auction slot");
+    XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyVote : has auction slot");
 
     // Update the vote entries and the trading/discounted fee.
     ammSle->setFieldArray(sfVoteSlots, updatedVoteSlots);
diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp
index 3672c4a68a..d4b6ba5429 100644
--- a/src/test/app/AMM_test.cpp
+++ b/src/test/app/AMM_test.cpp
@@ -5747,38 +5747,16 @@ private:
         };
 
         // ledger is closed after each transaction, vote/withdraw don't fail
-        // regardless whether the amendment is enabled or not
         test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
-        test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
         // ledger is not closed after each transaction
-        // vote/withdraw don't fail if the amendment is enabled
         test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false);
-        // vote/withdraw fail if the amendment is not enabled
-        // second vote/withdraw still fail: second vote fails because
-        // the initial trading fee is 0, consequently second withdraw fails
-        // because the second vote fails
-        test(
-            all - fixInnerObjTemplate,
-            tefEXCEPTION,
-            tefEXCEPTION,
-            tefEXCEPTION,
-            tefEXCEPTION,
-            0,
-            false);
         // if non-zero trading/discounted fee then vote/withdraw
-        // don't fail whether the ledger is closed or not and
-        // the amendment is enabled or not
+        // don't fail whether the ledger is closed or not
         test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
-        test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
         test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
-        test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
         // non-zero trading fee but discounted fee is 0, vote doesn't fail
         // but withdraw fails
         test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false);
-        // second vote sets the trading fee to non-zero, consequently
-        // second withdraw doesn't fail even if the amendment is not
-        // enabled and the ledger is not closed
-        test(all - fixInnerObjTemplate, tesSUCCESS, tefEXCEPTION, tesSUCCESS, tesSUCCESS, 9, false);
     }
 
     void
diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp
index 2184f7e1b0..5c815d4226 100644
--- a/src/test/jtx/impl/AMM.cpp
+++ b/src/test/jtx/impl/AMM.cpp
@@ -738,8 +738,7 @@ AMM::bid(BidArg const& arg)
 {
     if (auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())))
     {
-        if (env_.current()->rules().enabled(fixInnerObjTemplate) &&
-            !amm->isFieldPresent(sfAuctionSlot))
+        if (!amm->isFieldPresent(sfAuctionSlot))
             Throw("AMM::Bid");
         if (amm->isFieldPresent(sfAuctionSlot))
         {
@@ -867,8 +866,7 @@ AMM::expectAuctionSlot(auto&& cb) const
 {
     if (auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())))
     {
-        if (env_.current()->rules().enabled(fixInnerObjTemplate) &&
-            !amm->isFieldPresent(sfAuctionSlot))
+        if (!amm->isFieldPresent(sfAuctionSlot))
             Throw("AMM::expectAuctionSlot");
         if (amm->isFieldPresent(sfAuctionSlot))
         {
@@ -876,10 +874,6 @@ AMM::expectAuctionSlot(auto&& cb) const
                 safeDowncast(amm->peekAtField(sfAuctionSlot));
             if (auctionSlot.isFieldPresent(sfAccount))
             {
-                // This could fail in pre-fixInnerObjTemplate tests
-                // if the submitted transactions recreate one of
-                // the failure scenarios. Access as optional
-                // to avoid the failure.
                 auto const slotFee = auctionSlot[~sfDiscountedFee].value_or(0);
                 auto const slotInterval = ammAuctionTimeSlot(
                     env_.app().getTimeKeeper().now().time_since_epoch().count(), auctionSlot);
diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp
index 043fdd2d7b..7f54f81423 100644
--- a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp
+++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp
@@ -214,9 +214,7 @@ doAMMInfo(RPC::JsonContext& context)
     }
     if (voteSlots.size() > 0)
         ammResult[jss::vote_slots] = std::move(voteSlots);
-    XRPL_ASSERT(
-        !ledger->rules().enabled(fixInnerObjTemplate) || amm->isFieldPresent(sfAuctionSlot),
-        "xrpl::doAMMInfo : auction slot is set");
+    XRPL_ASSERT(amm->isFieldPresent(sfAuctionSlot), "xrpl::doAMMInfo : auction slot is present");
     if (amm->isFieldPresent(sfAuctionSlot))
     {
         auto const& auctionSlot = safeDowncast(amm->peekAtField(sfAuctionSlot));

From 8e378c4f4727a188ca0300b76142f2f867ba1e30 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 2 Jul 2026 00:14:17 +0100
Subject: [PATCH 036/100] build: Add verify-headers target to cleanup headers
 (#7670)

---
 .clang-tidy                                   |   2 +-
 .gersemi/definitions.cmake                    |   6 +
 .../scripts/levelization/results/loops.txt    |   6 -
 .../scripts/levelization/results/ordering.txt |  11 +-
 .github/workflows/reusable-clang-tidy.yml     |   3 +-
 .pre-commit-config.yaml                       |   6 +-
 BUILD.md                                      |  38 +++-
 bin/pre-commit/clang_tidy_check.py            | 171 ++----------------
 cmake/XrplCore.cmake                          |   9 +
 cmake/XrplSettings.cmake                      |  17 ++
 cmake/add_module.cmake                        |  17 ++
 cmake/verify_headers.cmake                    |  84 +++++++++
 include/xrpl/basics/Buffer.h                  |   1 +
 include/xrpl/basics/CompressionAlgorithms.h   |   1 +
 include/xrpl/basics/DecayingSample.h          |   1 +
 include/xrpl/basics/FileUtilities.h           |   2 +
 include/xrpl/basics/IntrusivePointer.h        |   1 +
 include/xrpl/basics/IntrusiveRefCounts.h      |   1 +
 include/xrpl/basics/Log.h                     |   3 +-
 include/xrpl/basics/Number.h                  |   4 +
 include/xrpl/basics/RangeSet.h                |   1 +
 include/xrpl/basics/Resolver.h                |   1 +
 include/xrpl/basics/ResolverAsio.h            |   2 +
 include/xrpl/basics/SHAMapHash.h              |   2 +
 include/xrpl/basics/SharedWeakCachePointer.h  |   2 +
 include/xrpl/basics/SlabAllocator.h           |   1 +
 include/xrpl/basics/StringUtilities.h         |   3 +-
 include/xrpl/basics/TaggedCache.h             |  15 +-
 include/xrpl/basics/TaggedCache.ipp           |   1 +
 include/xrpl/basics/UnorderedContainers.h     |   4 +-
 include/xrpl/basics/base64.h                  |   1 +
 include/xrpl/basics/base_uint.h               |  10 +
 include/xrpl/basics/chrono.h                  |   1 +
 include/xrpl/basics/hardened_hash.h           |   1 -
 include/xrpl/basics/join.h                    |   2 +
 include/xrpl/basics/make_SSLContext.h         |   1 +
 .../xrpl/basics/partitioned_unordered_map.h   |   3 +
 include/xrpl/basics/random.h                  |   1 -
 include/xrpl/basics/safe_cast.h               |   2 -
 include/xrpl/basics/strHex.h                  |   3 +
 include/xrpl/basics/tagged_integer.h          |   1 +
 include/xrpl/beast/asio/io_latency_probe.h    |   1 +
 .../beast/container/aged_container_utility.h  |   1 +
 include/xrpl/beast/container/aged_map.h       |   1 +
 include/xrpl/beast/container/aged_multimap.h  |   1 +
 .../xrpl/beast/container/aged_unordered_map.h |   1 +
 .../beast/container/aged_unordered_multimap.h |   1 +
 .../container/detail/aged_ordered_container.h |  27 +--
 .../detail/aged_unordered_container.h         |   6 +
 include/xrpl/beast/core/CurrentThreadName.h   |   1 +
 include/xrpl/beast/core/LexicalCast.h         |   5 +-
 include/xrpl/beast/core/List.h                |   2 +
 include/xrpl/beast/core/LockFreeStack.h       |   3 +-
 include/xrpl/beast/hash/hash_append.h         |   2 +-
 include/xrpl/beast/insight/Collector.h        |   2 +
 include/xrpl/beast/insight/Insight.h          |  15 --
 include/xrpl/beast/insight/NullCollector.h    |   2 +
 include/xrpl/beast/insight/StatsDCollector.h  |   3 +
 include/xrpl/beast/net/IPAddress.h            |   1 +
 include/xrpl/beast/net/IPAddressV4.h          |   2 -
 include/xrpl/beast/net/IPAddressV6.h          |   2 -
 include/xrpl/beast/net/IPEndpoint.h           |   5 +
 include/xrpl/beast/rfc2616.h                  |   1 +
 include/xrpl/beast/test/yield_to.h            |   2 +
 include/xrpl/beast/unit_test.h                |   9 -
 include/xrpl/beast/unit_test/match.h          |   1 +
 include/xrpl/beast/unit_test/recorder.h       |   4 +
 include/xrpl/beast/unit_test/reporter.h       |   5 +-
 include/xrpl/beast/unit_test/results.h        |   1 +
 include/xrpl/beast/unit_test/suite.h          |   4 +-
 include/xrpl/beast/unit_test/suite_info.h     |   2 +-
 include/xrpl/beast/unit_test/suite_list.h     |   1 +
 include/xrpl/beast/unit_test/thread.h         |   2 +
 include/xrpl/beast/utility/Journal.h          |   3 +
 include/xrpl/beast/utility/PropertyStream.h   |   2 +
 include/xrpl/beast/utility/WrappedSink.h      |   1 +
 include/xrpl/beast/utility/rngfill.h          |   2 -
 include/xrpl/conditions/Condition.h           |   4 +
 include/xrpl/conditions/Fulfillment.h         |   5 +
 .../xrpl/conditions/detail/PreimageSha256.h   |   4 +
 include/xrpl/conditions/detail/utils.h        |   3 +
 include/xrpl/config/BasicConfig.h             |   4 +
 include/xrpl/core/ClosureCounter.h            |   3 +
 include/xrpl/core/HashRouter.h                |   6 +
 include/xrpl/core/Job.h                       |   5 +
 include/xrpl/core/JobQueue.h                  |  22 ++-
 include/xrpl/core/JobTypeData.h               |   3 +
 include/xrpl/core/JobTypeInfo.h               |   4 +
 include/xrpl/core/JobTypes.h                  |   4 +
 include/xrpl/core/LoadMonitor.h               |   1 +
 include/xrpl/core/PeerReservationTable.h      |   1 +
 include/xrpl/core/PerfLog.h                   |   3 +-
 include/xrpl/core/ServiceRegistry.h           |   6 +
 include/xrpl/core/StartUpType.h               |   2 +-
 include/xrpl/core/detail/semaphore.h          |   1 +
 include/xrpl/crypto/RFC1751.h                 |   1 +
 include/xrpl/crypto/csprng.h                  |   3 +
 include/xrpl/json/JsonPropertyStream.h        |   3 +
 include/xrpl/json/Writer.h                    |   4 +-
 include/xrpl/json/detail/json_assert.h        |   3 -
 include/xrpl/json/json_reader.h               |   3 +
 include/xrpl/json/json_writer.h               |   3 +
 include/xrpl/ledger/AcceptedLedgerTx.h        |  10 +
 include/xrpl/ledger/AmendmentTable.h          |  23 +++
 include/xrpl/ledger/ApplyView.h               |  15 +-
 include/xrpl/ledger/ApplyViewImpl.h           |  11 ++
 include/xrpl/ledger/BookDirs.h                |   7 +
 include/xrpl/ledger/CachedView.h              |   9 +
 include/xrpl/ledger/CanonicalTXSet.h          |   6 +
 include/xrpl/ledger/Dir.h                     |  10 +-
 include/xrpl/ledger/Ledger.h                  |  22 ++-
 include/xrpl/ledger/LedgerTiming.h            |   5 +-
 include/xrpl/ledger/OpenView.h                |  13 +-
 include/xrpl/ledger/PaymentSandbox.h          |  10 +-
 include/xrpl/ledger/RawView.h                 |   3 +
 include/xrpl/ledger/ReadView.h                |  11 +-
 include/xrpl/ledger/Sandbox.h                 |   2 +
 include/xrpl/ledger/View.h                    |   9 +
 include/xrpl/ledger/detail/ApplyStateTable.h  |  13 ++
 include/xrpl/ledger/detail/ApplyViewBase.h    |  10 +
 include/xrpl/ledger/detail/RawStateTable.h    |   8 +
 include/xrpl/ledger/detail/ReadViewFwdRange.h |   2 +
 include/xrpl/ledger/helpers/AMMHelpers.h      |  16 +-
 .../xrpl/ledger/helpers/AccountRootHelpers.h  |   5 +
 .../xrpl/ledger/helpers/CredentialHelpers.h   |   9 +-
 include/xrpl/ledger/helpers/DelegateHelpers.h |   3 +
 .../xrpl/ledger/helpers/DirectoryHelpers.h    |   6 +-
 include/xrpl/ledger/helpers/EscrowHelpers.h   |  17 +-
 include/xrpl/ledger/helpers/LendingHelpers.h  |  21 ++-
 include/xrpl/ledger/helpers/MPTokenHelpers.h  |   6 +
 include/xrpl/ledger/helpers/NFTokenHelpers.h  |  16 +-
 .../ledger/helpers/PaymentChannelHelpers.h    |   5 +-
 .../ledger/helpers/PermissionedDEXHelpers.h   |   6 +-
 .../xrpl/ledger/helpers/RippleStateHelpers.h  |   7 +
 include/xrpl/ledger/helpers/TokenHelpers.h    |   7 +
 include/xrpl/net/AutoSocket.h                 |   8 +
 include/xrpl/net/HTTPClient.h                 |   1 +
 include/xrpl/net/HTTPClientSSLContext.h       |   8 +
 include/xrpl/net/RegisterSSLCerts.h           |   2 +-
 include/xrpl/nodestore/Backend.h              |   9 +
 include/xrpl/nodestore/Database.h             |  18 +-
 include/xrpl/nodestore/DatabaseRotating.h     |   7 +
 include/xrpl/nodestore/DummyScheduler.h       |   1 +
 include/xrpl/nodestore/Factory.h              |   6 +-
 include/xrpl/nodestore/Manager.h              |   9 +-
 include/xrpl/nodestore/NodeObject.h           |   4 +
 include/xrpl/nodestore/Types.h                |   1 +
 include/xrpl/nodestore/detail/BatchWriter.h   |   2 +
 .../xrpl/nodestore/detail/DatabaseNodeImp.h   |  16 ++
 .../nodestore/detail/DatabaseRotatingImp.h    |  11 ++
 include/xrpl/nodestore/detail/DecodedBlob.h   |   2 +
 include/xrpl/nodestore/detail/EncodedBlob.h   |   3 +
 include/xrpl/nodestore/detail/ManagerImp.h    |  11 ++
 include/xrpl/nodestore/detail/codec.h         |   4 +
 include/xrpl/nodestore/detail/varint.h        |   1 +
 include/xrpl/proto/org/xrpl/rpc/v1/README.md  |   6 +-
 include/xrpl/protocol/AMMCore.h               |   5 +
 include/xrpl/protocol/AccountID.h             |   6 +-
 include/xrpl/protocol/AmountConversions.h     |  10 +
 include/xrpl/protocol/ApiVersion.h            |   1 +
 include/xrpl/protocol/Asset.h                 |  11 +-
 include/xrpl/protocol/Batch.h                 |   5 +-
 include/xrpl/protocol/Book.h                  |  11 ++
 include/xrpl/protocol/BuildInfo.h             |   1 +
 include/xrpl/protocol/ConfidentialTransfer.h  |  15 +-
 include/xrpl/protocol/ErrorCodes.h            |   3 +-
 include/xrpl/protocol/Feature.h               |   2 +
 include/xrpl/protocol/Fees.h                  |   3 +
 include/xrpl/protocol/IOUAmount.h             |   1 +
 include/xrpl/protocol/Indexes.h               |  11 +-
 include/xrpl/protocol/InnerObjectFormats.h    |   2 +
 include/xrpl/protocol/Issue.h                 |   6 +-
 include/xrpl/protocol/KnownFormats.h          |   4 +
 include/xrpl/protocol/LedgerFormats.h         |   3 +
 include/xrpl/protocol/LedgerHeader.h          |   3 +
 include/xrpl/protocol/MPTAmount.h             |   4 +-
 include/xrpl/protocol/MPTIssue.h              |  10 +
 include/xrpl/protocol/PathAsset.h             |   7 +
 include/xrpl/protocol/Permissions.h           |   5 +-
 include/xrpl/protocol/Protocol.h              |   2 +
 include/xrpl/protocol/PublicKey.h             |  10 +
 include/xrpl/protocol/Quality.h               |   6 +-
 include/xrpl/protocol/QualityFunction.h       |   6 +
 include/xrpl/protocol/Rate.h                  |   2 +-
 include/xrpl/protocol/Rules.h                 |   3 +
 include/xrpl/protocol/SField.h                |   3 +-
 include/xrpl/protocol/SOTemplate.h            |   2 +
 include/xrpl/protocol/STAccount.h             |   4 +
 include/xrpl/protocol/STAmount.h              |  16 +-
 include/xrpl/protocol/STArray.h               |  11 ++
 include/xrpl/protocol/STBase.h                |   3 +
 include/xrpl/protocol/STBitString.h           |   7 +
 include/xrpl/protocol/STBlob.h                |   6 +-
 include/xrpl/protocol/STCurrency.h            |   6 +-
 include/xrpl/protocol/STExchange.h            |   3 +-
 include/xrpl/protocol/STInteger.h             |   8 +
 include/xrpl/protocol/STIssue.h               |   9 +
 include/xrpl/protocol/STLedgerEntry.h         |  14 +-
 include/xrpl/protocol/STNumber.h              |   7 +
 include/xrpl/protocol/STObject.h              |  14 +-
 include/xrpl/protocol/STParsedJSON.h          |   5 +-
 include/xrpl/protocol/STPathSet.h             |   5 +-
 include/xrpl/protocol/STTakesAsset.h          |   2 +
 include/xrpl/protocol/STTx.h                  |  14 +-
 include/xrpl/protocol/STValidation.h          |  43 +++--
 include/xrpl/protocol/STVector256.h           |  10 +-
 include/xrpl/protocol/STXChainBridge.h        |  10 +
 include/xrpl/protocol/SecretKey.h             |   4 +
 include/xrpl/protocol/Seed.h                  |   3 +
 include/xrpl/protocol/Serializer.h            |   3 +-
 include/xrpl/protocol/Sign.h                  |   4 +
 include/xrpl/protocol/SystemParameters.h      |   3 +
 include/xrpl/protocol/TER.h                   |   2 +
 include/xrpl/protocol/TxFormats.h             |   2 +
 include/xrpl/protocol/TxMeta.h                |  11 +-
 include/xrpl/protocol/UintTypes.h             |   6 +-
 include/xrpl/protocol/Units.h                 |   4 +
 include/xrpl/protocol/XChainAttestations.h    |   8 +-
 include/xrpl/protocol/XRPAmount.h             |   5 +
 include/xrpl/protocol/detail/STVar.h          |   1 +
 include/xrpl/protocol/detail/b58_utils.h      |   4 +-
 include/xrpl/protocol/detail/token_errors.h   |   2 +
 include/xrpl/protocol/digest.h                |   3 +
 include/xrpl/protocol/json_get_or_throw.h     |   4 +
 include/xrpl/protocol/messages.h              |   2 -
 include/xrpl/protocol/serialize.h             |   3 +
 include/xrpl/protocol/st.h                    |  18 --
 include/xrpl/protocol/tokens.h                |   3 +-
 include/xrpl/rdb/DatabaseCon.h                |  12 +-
 include/xrpl/rdb/RelationalDatabase.h         |  18 +-
 include/xrpl/rdb/SociDB.h                     |   5 +-
 include/xrpl/resource/Charge.h                |   2 +
 include/xrpl/resource/Consumer.h              |   5 +-
 include/xrpl/resource/ResourceManager.h       |   4 +
 include/xrpl/resource/Types.h                 |  10 -
 include/xrpl/resource/detail/Entry.h          |   7 +-
 include/xrpl/resource/detail/Import.h         |   2 +
 include/xrpl/resource/detail/Key.h            |   3 +-
 include/xrpl/resource/detail/Logic.h          |  11 +-
 include/xrpl/server/InfoSub.h                 |   9 +
 include/xrpl/server/LoadFeeTrack.h            |   1 +
 include/xrpl/server/Manifest.h                |   8 +
 include/xrpl/server/NetworkOPs.h              |  15 +-
 include/xrpl/server/Port.h                    |   3 +-
 include/xrpl/server/Server.h                  |   4 +-
 include/xrpl/server/Session.h                 |   6 +-
 include/xrpl/server/SimpleWriter.h            |   5 +-
 include/xrpl/server/State.h                   |   4 +-
 include/xrpl/server/Vacuum.h                  |   1 +
 include/xrpl/server/WSSession.h               |   3 +-
 include/xrpl/server/Wallet.h                  |  12 ++
 include/xrpl/server/Writer.h                  |   1 +
 include/xrpl/server/detail/BaseHTTPPeer.h     |  11 +-
 include/xrpl/server/detail/BasePeer.h         |   3 +-
 include/xrpl/server/detail/BaseWSPeer.h       |   8 +
 include/xrpl/server/detail/Door.h             |   5 +
 include/xrpl/server/detail/JSONRPCUtil.h      |   3 +-
 include/xrpl/server/detail/LowestLayer.h      |   9 -
 include/xrpl/server/detail/PlainHTTPPeer.h    |   5 +
 include/xrpl/server/detail/PlainWSPeer.h      |   4 +
 include/xrpl/server/detail/SSLHTTPPeer.h      |   6 +
 include/xrpl/server/detail/SSLWSPeer.h        |   7 +-
 include/xrpl/server/detail/ServerImpl.h       |  11 +-
 include/xrpl/server/detail/Spawn.h            |   1 +
 include/xrpl/server/detail/io_list.h          |   1 +
 include/xrpl/shamap/Family.h                  |   2 +
 include/xrpl/shamap/FullBelowCache.h          |   4 +
 include/xrpl/shamap/SHAMap.h                  |  19 +-
 .../xrpl/shamap/SHAMapAccountStateLeafNode.h  |   7 +
 include/xrpl/shamap/SHAMapInnerNode.h         |   6 +-
 include/xrpl/shamap/SHAMapItem.h              |   8 +
 include/xrpl/shamap/SHAMapLeafNode.h          |   3 +
 include/xrpl/shamap/SHAMapMissingNode.h       |   4 +-
 include/xrpl/shamap/SHAMapNodeID.h            |   2 +
 include/xrpl/shamap/SHAMapSyncFilter.h        |   3 +
 include/xrpl/shamap/SHAMapTreeNode.h          |   2 +-
 include/xrpl/shamap/SHAMapTxLeafNode.h        |   7 +
 .../xrpl/shamap/SHAMapTxPlusMetaLeafNode.h    |   7 +
 include/xrpl/shamap/TreeNodeCache.h           |   1 +
 include/xrpl/shamap/detail/TaggedPointer.h    |   7 +-
 include/xrpl/tx/ApplyContext.h                |  11 ++
 include/xrpl/tx/SignerEntries.h               |   8 +-
 include/xrpl/tx/Transactor.h                  |  16 ++
 include/xrpl/tx/apply.h                       |   6 +-
 include/xrpl/tx/applySteps.h                  |  14 +-
 include/xrpl/tx/invariants/AMMInvariant.h     |   3 +
 .../xrpl/tx/invariants/DirectoryInvariant.h   |   2 +
 include/xrpl/tx/invariants/FreezeInvariant.h  |   3 +
 include/xrpl/tx/invariants/InvariantCheck.h   |   7 +-
 .../tx/invariants/InvariantCheckPrivilege.h   |   1 +
 .../xrpl/tx/invariants/LoanBrokerInvariant.h  |   2 +
 include/xrpl/tx/invariants/LoanInvariant.h    |   3 +
 include/xrpl/tx/invariants/MPTInvariant.h     |   8 +
 include/xrpl/tx/invariants/NFTInvariant.h     |   3 +-
 .../tx/invariants/PermissionedDEXInvariant.h  |   3 +
 .../invariants/PermissionedDomainInvariant.h  |   3 +
 include/xrpl/tx/invariants/VaultInvariant.h   |   5 +
 include/xrpl/tx/paths/AMMLiquidity.h          |  10 +-
 include/xrpl/tx/paths/AMMOffer.h              |   8 +-
 include/xrpl/tx/paths/BookTip.h               |   7 +-
 include/xrpl/tx/paths/Flow.h                  |   7 +
 include/xrpl/tx/paths/Offer.h                 |  12 +-
 include/xrpl/tx/paths/OfferStream.h           |   7 +-
 include/xrpl/tx/paths/RippleCalc.h            |   5 +
 include/xrpl/tx/paths/detail/EitherAmount.h   |   7 +-
 include/xrpl/tx/paths/detail/FlowDebugInfo.h  |  12 +-
 include/xrpl/tx/paths/detail/StepChecks.h     |   6 +
 include/xrpl/tx/paths/detail/Steps.h          |  16 +-
 include/xrpl/tx/paths/detail/StrandFlow.h     |  21 ++-
 .../tx/transactors/account/AccountDelete.h    |   7 +
 .../xrpl/tx/transactors/account/AccountSet.h  |  10 +-
 .../tx/transactors/account/SetRegularKey.h    |   7 +
 .../tx/transactors/account/SignerListSet.h    |   9 +
 .../xrpl/tx/transactors/bridge/XChainBridge.h |  11 +-
 .../xrpl/tx/transactors/check/CheckCancel.h   |   7 +
 include/xrpl/tx/transactors/check/CheckCash.h |   7 +
 .../xrpl/tx/transactors/check/CheckCreate.h   |   7 +
 .../credentials/CredentialAccept.h            |   9 +
 .../credentials/CredentialCreate.h            |   9 +
 .../credentials/CredentialDelete.h            |   9 +
 .../tx/transactors/delegate/DelegateSet.h     |   8 +
 include/xrpl/tx/transactors/dex/AMMBid.h      |   7 +
 include/xrpl/tx/transactors/dex/AMMClawback.h |  13 ++
 include/xrpl/tx/transactors/dex/AMMCreate.h   |   7 +
 include/xrpl/tx/transactors/dex/AMMDelete.h   |   7 +
 include/xrpl/tx/transactors/dex/AMMDeposit.h  |  14 ++
 include/xrpl/tx/transactors/dex/AMMVote.h     |   7 +
 include/xrpl/tx/transactors/dex/AMMWithdraw.h |  18 +-
 include/xrpl/tx/transactors/dex/OfferCancel.h |   8 +-
 include/xrpl/tx/transactors/dex/OfferCreate.h |  20 ++
 include/xrpl/tx/transactors/did/DIDDelete.h   |  10 +
 include/xrpl/tx/transactors/did/DIDSet.h      |   7 +
 .../xrpl/tx/transactors/escrow/EscrowCancel.h |   7 +
 .../xrpl/tx/transactors/escrow/EscrowCreate.h |   7 +
 .../xrpl/tx/transactors/escrow/EscrowFinish.h |   7 +
 .../lending/LoanBrokerCoverClawback.h         |   7 +
 .../lending/LoanBrokerCoverDeposit.h          |   7 +
 .../lending/LoanBrokerCoverWithdraw.h         |   7 +
 .../tx/transactors/lending/LoanBrokerDelete.h |   7 +
 .../tx/transactors/lending/LoanBrokerSet.h    |  10 +
 .../xrpl/tx/transactors/lending/LoanDelete.h  |   7 +
 .../xrpl/tx/transactors/lending/LoanManage.h  |  11 ++
 include/xrpl/tx/transactors/lending/LoanPay.h |   9 +
 include/xrpl/tx/transactors/lending/LoanSet.h |  13 +-
 .../tx/transactors/nft/NFTokenAcceptOffer.h   |  10 +
 include/xrpl/tx/transactors/nft/NFTokenBurn.h |   7 +
 .../tx/transactors/nft/NFTokenCancelOffer.h   |   7 +
 .../tx/transactors/nft/NFTokenCreateOffer.h   |   9 +
 include/xrpl/tx/transactors/nft/NFTokenMint.h |  12 +-
 .../xrpl/tx/transactors/nft/NFTokenModify.h   |   7 +
 .../xrpl/tx/transactors/oracle/OracleDelete.h |   9 +
 .../xrpl/tx/transactors/oracle/OracleSet.h    |   7 +
 .../tx/transactors/payment/DepositPreauth.h   |   9 +
 include/xrpl/tx/transactors/payment/Payment.h |  12 ++
 .../payment_channel/PaymentChannelClaim.h     |   9 +
 .../payment_channel/PaymentChannelCreate.h    |   7 +
 .../payment_channel/PaymentChannelFund.h      |   7 +
 .../PermissionedDomainDelete.h                |   7 +
 .../PermissionedDomainSet.h                   |   7 +
 include/xrpl/tx/transactors/system/Batch.h    |  11 ++
 include/xrpl/tx/transactors/system/Change.h   |   7 +
 .../tx/transactors/system/LedgerStateFix.h    |   9 +
 .../xrpl/tx/transactors/system/TicketCreate.h |   9 +
 include/xrpl/tx/transactors/token/Clawback.h  |   7 +
 .../token/ConfidentialMPTClawback.h           |   9 +
 .../token/ConfidentialMPTConvert.h            |   9 +
 .../token/ConfidentialMPTConvertBack.h        |   9 +
 .../token/ConfidentialMPTMergeInbox.h         |   9 +
 .../transactors/token/ConfidentialMPTSend.h   |   9 +
 .../tx/transactors/token/MPTokenAuthorize.h   |  12 ++
 .../transactors/token/MPTokenIssuanceCreate.h |  13 ++
 .../token/MPTokenIssuanceDestroy.h            |   7 +
 .../tx/transactors/token/MPTokenIssuanceSet.h |   9 +
 include/xrpl/tx/transactors/token/TrustSet.h  |  12 +-
 .../xrpl/tx/transactors/vault/VaultClawback.h |  10 +
 .../xrpl/tx/transactors/vault/VaultCreate.h   |   9 +
 .../xrpl/tx/transactors/vault/VaultDelete.h   |   7 +
 .../xrpl/tx/transactors/vault/VaultDeposit.h  |   7 +
 include/xrpl/tx/transactors/vault/VaultSet.h  |   7 +
 .../xrpl/tx/transactors/vault/VaultWithdraw.h |   7 +
 src/libxrpl/core/detail/JobQueue.cpp          |   1 +
 src/libxrpl/json/json_value.cpp               |   2 +
 src/libxrpl/net/RegisterSSLCerts.cpp          |   2 +
 src/libxrpl/protocol/AMMCore.cpp              |   1 +
 src/libxrpl/protocol/ConfidentialTransfer.cpp |   1 +
 src/libxrpl/protocol/Permissions.cpp          |   1 +
 src/libxrpl/protocol/STNumber.cpp             |   2 +-
 src/libxrpl/server/State.cpp                  |   1 +
 src/libxrpl/server/Wallet.cpp                 |   2 +
 .../PermissionedDomainInvariant.cpp           |   1 +
 .../tx/transactors/dex/OfferCreate.cpp        |   1 +
 .../tx/transactors/nft/NFTokenCreateOffer.cpp |   1 +
 src/libxrpl/tx/transactors/system/Batch.cpp   |   1 +
 src/test/app/FlowMPT_test.cpp                 |   1 +
 src/test/app/LedgerReplay_test.cpp            |   1 +
 src/test/app/MultiSign_test.cpp               |   1 +
 src/test/app/OfferMPT_test.cpp                |   2 +
 src/test/app/PayStrand_test.cpp               |   1 +
 src/test/app/SHAMapStore_test.cpp             |   1 +
 src/test/basics/hardened_hash_test.cpp        |   1 +
 src/test/beast/IPEndpointCommon.h             |   4 +
 .../beast/aged_associative_container_test.cpp |  20 +-
 src/test/core/SociDB_test.cpp                 |   1 +
 src/test/csf.h                                |  19 --
 src/test/csf/BasicNetwork.h                   |   2 +
 src/test/csf/CollectorRef.h                   |   7 +
 src/test/csf/Digraph.h                        |   5 +-
 src/test/csf/Histogram.h                      |   4 +-
 src/test/csf/Peer.h                           |  20 +-
 src/test/csf/PeerGroup.h                      |   9 +
 src/test/csf/Scheduler.h                      |   1 +
 src/test/csf/Sim.h                            |   8 +-
 src/test/csf/TrustGraph.h                     |   6 +-
 src/test/csf/Tx.h                             |   1 +
 src/test/csf/Validation.h                     |   4 +-
 src/test/csf/collectors.h                     |  10 +
 src/test/csf/events.h                         |   5 +-
 src/test/csf/ledgers.h                        |   8 +-
 src/test/csf/random.h                         |   3 +
 src/test/csf/submitters.h                     |   3 +-
 src/test/csf/timers.h                         |   1 +
 src/test/json/TestOutputSuite.h               |   9 +-
 src/test/jtx.h                                |  60 ------
 src/test/jtx/AMM.h                            |  23 ++-
 src/test/jtx/AMMTest.h                        |  12 ++
 src/test/jtx/AbstractClient.h                 |   2 +
 src/test/jtx/Account.h                        |   4 +-
 src/test/jtx/CaptureLogs.h                    |   6 +
 src/test/jtx/CheckMessageLogs.h               |   5 +
 src/test/jtx/ConfidentialTransfer.h           |  22 ---
 src/test/jtx/Env.h                            |  27 ++-
 src/test/jtx/Env_ss.h                         |   6 +
 src/test/jtx/JTx.h                            |   3 +
 src/test/jtx/Oracle.h                         |  21 ++-
 src/test/jtx/PathSet.h                        |  16 +-
 src/test/jtx/TestHelpers.h                    |  33 +++-
 src/test/jtx/TestSuite.h                      |   4 +-
 src/test/jtx/TrustedPublisherServer.h         |  19 ++
 src/test/jtx/WSClient.h                       |   5 +
 src/test/jtx/account_txn_id.h                 |   3 +
 src/test/jtx/acctdelete.h                     |   4 +-
 src/test/jtx/amount.h                         |  14 +-
 src/test/jtx/balance.h                        |   4 +
 src/test/jtx/batch.h                          |  10 +-
 src/test/jtx/check.h                          |   6 +-
 src/test/jtx/credentials.h                    |  13 +-
 src/test/jtx/delegate.h                       |   6 +
 src/test/jtx/delivermin.h                     |   1 +
 src/test/jtx/deposit.h                        |   9 +-
 src/test/jtx/did.h                            |   8 +-
 src/test/jtx/directory.h                      |   6 +-
 src/test/jtx/domain.h                         |   3 +
 src/test/jtx/envconfig.h                      |   5 +
 src/test/jtx/escrow.h                         |  11 +-
 src/test/jtx/fee.h                            |   3 +
 src/test/jtx/flags.h                          |   4 +
 src/test/jtx/impl/Oracle.cpp                  |   1 +
 src/test/jtx/invoice_id.h                     |   3 +
 src/test/jtx/jtx_json.h                       |   3 +
 src/test/jtx/last_ledger_sequence.h           |   3 +
 src/test/jtx/ledgerStateFix.h                 |   4 +-
 src/test/jtx/memo.h                           |   2 +
 src/test/jtx/mpt.h                            |  20 ++
 src/test/jtx/multisign.h                      |  10 +-
 src/test/jtx/noop.h                           |   3 +
 src/test/jtx/offer.h                          |   2 +
 src/test/jtx/owners.h                         |   3 +-
 src/test/jtx/paths.h                          |   7 +-
 src/test/jtx/pay.h                            |   1 +
 src/test/jtx/permissioned_dex.h               |   5 +
 src/test/jtx/permissioned_domains.h           |  12 ++
 src/test/jtx/prop.h                           |   2 +
 src/test/jtx/quality.h                        |   3 +
 src/test/jtx/require.h                        |   1 +
 src/test/jtx/rpc.h                            |   7 +-
 src/test/jtx/sendmax.h                        |   1 +
 src/test/jtx/seq.h                            |   2 +
 src/test/jtx/sig.h                            |   5 +
 src/test/jtx/tag.h                            |   3 +
 src/test/jtx/ter.h                            |   4 +
 src/test/jtx/ticket.h                         |   4 +
 src/test/jtx/token.h                          |   8 +-
 src/test/jtx/trust.h                          |   3 +
 src/test/jtx/txflags.h                        |   3 +
 src/test/jtx/utility.h                        |   4 +-
 src/test/jtx/vault.h                          |   2 +-
 src/test/jtx/xchain_bridge.h                  |   9 +-
 src/test/nodestore/TestBase.h                 |  11 +-
 src/test/protocol/STObject_test.cpp           |   1 +
 src/test/protocol/STParsedJSON_test.cpp       |   1 +
 src/test/rpc/GRPCTestClientBase.h             |  32 ----
 src/test/shamap/common.h                      |  13 ++
 src/test/unit_test/FileDirGuard.h             |   8 +-
 src/test/unit_test/SuiteJournal.h             |   7 +-
 src/test/unit_test/multi_runner.h             |   9 +-
 src/tests/libxrpl/CMakeLists.txt              |   7 +
 src/tests/libxrpl/helpers/IOU.h               |   2 +-
 src/tests/libxrpl/helpers/TestFamily.h        |   9 +
 .../libxrpl/helpers/TestServiceRegistry.h     |   5 +
 src/tests/libxrpl/helpers/TestSink.h          |   2 +
 src/tests/libxrpl/helpers/TxTest.h            |  10 +-
 .../libxrpl/protocol_autogen/TestHelpers.h    |   1 +
 .../app/consensus/RCLCensorshipDetector.h     |   2 +-
 src/xrpld/app/consensus/RCLConsensus.h        |  20 +-
 src/xrpld/app/consensus/RCLCxLedger.h         |   8 +-
 src/xrpld/app/consensus/RCLCxPeerPos.h        |   3 +
 src/xrpld/app/consensus/RCLCxTx.h             |   8 +
 src/xrpld/app/consensus/RCLValidations.h      |  11 ++
 src/xrpld/app/ledger/AcceptedLedger.h         |   5 +
 src/xrpld/app/ledger/AccountStateSF.h         |   6 +
 src/xrpld/app/ledger/BuildLedger.h            |   4 +
 src/xrpld/app/ledger/ConsensusTransSetSF.cpp  |   1 +
 src/xrpld/app/ledger/ConsensusTransSetSF.h    |   7 +
 src/xrpld/app/ledger/InboundLedger.h          |  17 ++
 src/xrpld/app/ledger/InboundLedgers.h         |  14 ++
 src/xrpld/app/ledger/InboundTransactions.h    |   7 +
 src/xrpld/app/ledger/LedgerCleaner.h          |   2 +
 src/xrpld/app/ledger/LedgerHistory.h          |   7 +
 src/xrpld/app/ledger/LedgerHolder.h           |   3 +
 src/xrpld/app/ledger/LedgerMaster.h           |  21 ++-
 src/xrpld/app/ledger/LedgerPersistence.h      |   4 +
 src/xrpld/app/ledger/LedgerReplayTask.h       |   5 +
 src/xrpld/app/ledger/LedgerReplayer.h         |  11 +-
 src/xrpld/app/ledger/LedgerToJson.h           |   8 +-
 src/xrpld/app/ledger/LocalTxs.h               |   3 +
 src/xrpld/app/ledger/OpenLedger.h             |  10 +-
 src/xrpld/app/ledger/OrderBookDBImpl.h        |  11 ++
 src/xrpld/app/ledger/TransactionMaster.h      |   9 +
 src/xrpld/app/ledger/TransactionStateSF.h     |   6 +
 .../app/ledger/detail/LedgerDeltaAcquire.cpp  |   2 +
 .../app/ledger/detail/LedgerDeltaAcquire.h    |  10 +
 src/xrpld/app/ledger/detail/LedgerMaster.cpp  |   1 +
 .../ledger/detail/LedgerReplayMsgHandler.h    |   5 +-
 .../app/ledger/detail/LedgerReplayTask.cpp    |   1 +
 .../app/ledger/detail/SkipListAcquire.cpp     |   2 +
 src/xrpld/app/ledger/detail/SkipListAcquire.h |  12 +-
 src/xrpld/app/ledger/detail/TimeoutCounter.h  |   6 +
 .../app/ledger/detail/TransactionAcquire.h    |  11 ++
 src/xrpld/app/main/Application.cpp            |   1 +
 src/xrpld/app/main/Application.h              |  12 +-
 src/xrpld/app/main/BasicApp.h                 |   1 +
 src/xrpld/app/main/CollectorManager.h         |   7 +-
 src/xrpld/app/main/GRPCServer.cpp             |   1 +
 src/xrpld/app/main/GRPCServer.h               |  15 +-
 src/xrpld/app/main/LoadManager.h              |   2 +-
 src/xrpld/app/main/NodeIdentity.h             |   2 +
 src/xrpld/app/main/NodeStoreScheduler.h       |   1 +
 src/xrpld/app/main/Tuning.h                   |   1 +
 src/xrpld/app/misc/AmendmentTableImpl.h       |   6 +-
 src/xrpld/app/misc/FeeVote.h                  |   6 +
 src/xrpld/app/misc/FeeVoteImpl.cpp            |   1 +
 src/xrpld/app/misc/NegativeUNLVote.h          |   7 +
 src/xrpld/app/misc/SHAMapStore.h              |   7 +-
 src/xrpld/app/misc/SHAMapStoreImp.cpp         |   1 +
 src/xrpld/app/misc/SHAMapStoreImp.h           |  16 ++
 src/xrpld/app/misc/Transaction.h              |   9 +
 src/xrpld/app/misc/TxQ.h                      |  18 ++
 src/xrpld/app/misc/ValidatorKeys.h            |   2 +
 src/xrpld/app/misc/ValidatorList.h            |  17 +-
 src/xrpld/app/misc/ValidatorSite.h            |   9 +-
 src/xrpld/app/misc/detail/AccountTxPaging.h   |   3 +
 src/xrpld/app/misc/detail/ValidatorSite.cpp   |   1 -
 src/xrpld/app/misc/detail/WorkBase.h          |   4 +-
 src/xrpld/app/misc/detail/WorkFile.h          |   3 +
 src/xrpld/app/misc/detail/WorkPlain.h         |   3 +
 src/xrpld/app/misc/detail/WorkSSL.h           |   5 +-
 src/xrpld/app/misc/make_NetworkOPs.h          |   3 +-
 src/xrpld/app/rdb/PeerFinder.h                |   7 +-
 src/xrpld/app/rdb/backend/SQLiteDatabase.h    |  12 ++
 src/xrpld/app/rdb/backend/detail/Node.cpp     |   2 +
 src/xrpld/app/rdb/backend/detail/Node.h       |  24 +++
 src/xrpld/app/rdb/detail/PeerFinder.cpp       |   1 +
 src/xrpld/consensus/Consensus.h               |  11 +-
 src/xrpld/consensus/ConsensusParms.h          |   2 +-
 src/xrpld/consensus/ConsensusProposal.h       |   1 +
 src/xrpld/consensus/ConsensusTypes.h          |   4 +
 src/xrpld/consensus/DisputedTx.h              |   5 +
 src/xrpld/consensus/LedgerTrie.h              |   4 +
 src/xrpld/consensus/Validations.h             |  11 +-
 src/xrpld/core/Config.h                       |   4 +
 src/xrpld/core/TimeKeeper.h                   |   1 +
 src/xrpld/overlay/Cluster.h                   |   5 +
 src/xrpld/overlay/Compression.h               |   4 +
 src/xrpld/overlay/Message.h                   |  11 +-
 src/xrpld/overlay/Overlay.h                   |  11 ++
 src/xrpld/overlay/Peer.h                      |   6 +
 src/xrpld/overlay/PeerSet.h                   |   9 +
 src/xrpld/overlay/ReduceRelayCommon.h         |   2 +
 src/xrpld/overlay/Slot.h                      |  18 +-
 src/xrpld/overlay/Squelch.h                   |   1 +
 src/xrpld/overlay/detail/ConnectAttempt.h     |  12 ++
 src/xrpld/overlay/detail/Handshake.h          |   6 +-
 src/xrpld/overlay/detail/OverlayImpl.h        |  22 ++-
 src/xrpld/overlay/detail/PeerImp.h            |  31 ++++
 src/xrpld/overlay/detail/ProtocolMessage.h    |   9 +-
 src/xrpld/overlay/detail/TrafficCount.h       |   8 +-
 src/xrpld/overlay/detail/TxMetrics.h          |   4 +-
 src/xrpld/overlay/detail/ZeroCopyStream.h     |  11 +-
 src/xrpld/overlay/make_Overlay.h              |   7 +
 src/xrpld/overlay/predicates.h                |   1 +
 src/xrpld/peerfinder/PeerfinderManager.h      |   9 +
 src/xrpld/peerfinder/Slot.h                   |   2 +
 src/xrpld/peerfinder/detail/Bootcache.h       |   1 +
 src/xrpld/peerfinder/detail/Checker.h         |   2 +
 src/xrpld/peerfinder/detail/Counts.h          |   7 +-
 src/xrpld/peerfinder/detail/Fixed.h           |   5 +
 src/xrpld/peerfinder/detail/Handouts.h        |   5 +
 src/xrpld/peerfinder/detail/Livecache.h       |  14 ++
 src/xrpld/peerfinder/detail/Logic.h           |  16 ++
 src/xrpld/peerfinder/detail/SlotImp.h         |   5 +
 src/xrpld/peerfinder/detail/Source.h          |   4 +
 src/xrpld/peerfinder/detail/SourceStrings.h   |   2 +
 src/xrpld/peerfinder/detail/Store.h           |   6 +
 src/xrpld/peerfinder/detail/StoreSqdb.h       |   9 +
 src/xrpld/peerfinder/detail/Tuning.h          |   3 +
 src/xrpld/peerfinder/detail/iosformat.h       |   3 +
 src/xrpld/peerfinder/make_Manager.h           |   4 +
 src/xrpld/perflog/detail/PerfLogImp.cpp       |   3 +
 src/xrpld/perflog/detail/PerfLogImp.h         |   8 +-
 src/xrpld/rpc/BookChanges.h                   |  12 ++
 src/xrpld/rpc/CTID.h                          |   7 +-
 src/xrpld/rpc/Context.h                       |   6 +
 src/xrpld/rpc/DeliveredAmount.h               |   2 +-
 src/xrpld/rpc/GRPCHandlers.h                  |   8 +-
 src/xrpld/rpc/MPTokenIssuanceID.h             |   2 +-
 src/xrpld/rpc/Output.h                        |   9 +-
 src/xrpld/rpc/RPCCall.h                       |   5 +-
 src/xrpld/rpc/RPCHandler.h                    |   5 +
 src/xrpld/rpc/RPCSub.h                        |   3 +
 src/xrpld/rpc/Role.h                          |   4 +-
 src/xrpld/rpc/ServerHandler.h                 |  17 +-
 src/xrpld/rpc/Status.h                        |   7 +
 src/xrpld/rpc/detail/AccountAssets.h          |   6 +-
 src/xrpld/rpc/detail/AssetCache.h             |   6 +-
 src/xrpld/rpc/detail/Handler.h                |  10 +-
 src/xrpld/rpc/detail/MPT.h                    |   2 +-
 src/xrpld/rpc/detail/PathRequest.h            |  16 +-
 src/xrpld/rpc/detail/PathRequestManager.h     |  10 +
 src/xrpld/rpc/detail/Pathfinder.h             |  16 +-
 src/xrpld/rpc/detail/PathfinderUtils.h        |   4 +
 src/xrpld/rpc/detail/RPCHandler.cpp           |   1 +
 src/xrpld/rpc/detail/RPCHelpers.h             |  15 +-
 src/xrpld/rpc/detail/RPCLedgerHelpers.cpp     |   4 +
 src/xrpld/rpc/detail/RPCLedgerHelpers.h       |   9 +-
 src/xrpld/rpc/detail/TransactionSign.h        |   5 +
 src/xrpld/rpc/detail/TrustLine.h              |   7 +-
 src/xrpld/rpc/detail/Tuning.h                 |   2 +
 src/xrpld/rpc/detail/WSInfoSub.h              |   3 +
 src/xrpld/rpc/handlers/Handlers.h             |   2 +
 .../rpc/handlers/account/AccountNFTs.cpp      |   1 +
 .../admin/peer/PeerReservationsDel.cpp        |   1 +
 .../admin/peer/PeerReservationsList.cpp       |   1 +
 .../rpc/handlers/admin/status/GetCounts.h     |   2 +
 src/xrpld/rpc/handlers/ledger/Ledger.h        |   8 +-
 .../rpc/handlers/ledger/LedgerEntryHelpers.h  |  17 +-
 .../rpc/handlers/orderbook/BookChanges.cpp    |   1 +
 .../handlers/orderbook/GetAggregatePrice.cpp  |   1 +
 .../rpc/handlers/orderbook/NFTOffersHelpers.h |  12 +-
 src/xrpld/rpc/handlers/server_info/Version.h  |   7 +
 src/xrpld/rpc/json_body.h                     |   5 +
 src/xrpld/shamap/NodeFamily.cpp               |   1 +
 src/xrpld/shamap/NodeFamily.h                 |   9 +
 662 files changed, 3870 insertions(+), 743 deletions(-)
 create mode 100644 cmake/verify_headers.cmake
 delete mode 100644 include/xrpl/beast/insight/Insight.h
 delete mode 100644 include/xrpl/protocol/st.h
 delete mode 100644 include/xrpl/resource/Types.h
 delete mode 100644 src/test/csf.h
 delete mode 100644 src/test/jtx.h
 delete mode 100644 src/test/rpc/GRPCTestClientBase.h

diff --git a/.clang-tidy b/.clang-tidy
index e12c73cc56..84849db7a0 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -147,7 +147,7 @@ CheckOptions:
   bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true
   bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc
 
-  misc-include-cleaner.IgnoreHeaders: ".*/(detail|impl)/.*;.*fwd\\.h(pp)?;time.h;stdlib.h;sqlite3.h;netinet/in\\.h;sys/resource\\.h;sys/sysinfo\\.h;linux/sysinfo\\.h;__chrono/.*;bits/.*;_abort\\.h;boost/uuid/uuid_hash.hpp;boost/beast/core/flat_buffer\\.hpp;boost/beast/http/field\\.hpp;boost/beast/http/dynamic_body\\.hpp;boost/beast/http/message\\.hpp;boost/beast/http/read\\.hpp;boost/beast/http/write\\.hpp;openssl/obj_mac\\.h"
+  misc-include-cleaner.IgnoreHeaders: ".*/(detail|impl)/.*;.*fwd\\.h(pp)?;time.h;stdlib.h;sqlite3.h;netinet/in\\.h;sys/resource\\.h;sys/sysinfo\\.h;linux/sysinfo\\.h;__chrono/.*;bits/.*;_abort\\.h;boost/.*;openssl/obj_mac\\.h"
 
   readability-braces-around-statements.ShortStatementLines: 2
   readability-identifier-naming.MacroDefinitionCase: UPPER_CASE
diff --git a/.gersemi/definitions.cmake b/.gersemi/definitions.cmake
index 58bc74c70a..aa63076c8b 100644
--- a/.gersemi/definitions.cmake
+++ b/.gersemi/definitions.cmake
@@ -48,6 +48,12 @@ endfunction()
 function(add_module parent name)
 endfunction()
 
+function(verify_target_headers target headers_dir)
+endfunction()
+
+function(_verify_add_headers target dir)
+endfunction()
+
 function(setup_protocol_autogen)
 endfunction()
 
diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt
index fb449441e3..cf70468e32 100644
--- a/.github/scripts/levelization/results/loops.txt
+++ b/.github/scripts/levelization/results/loops.txt
@@ -1,9 +1,3 @@
-Loop: test.jtx test.toplevel
-  test.toplevel > test.jtx
-
-Loop: test.jtx test.unit_test
-  test.unit_test ~= test.jtx
-
 Loop: xrpld.app xrpld.overlay
   xrpld.app > xrpld.overlay
 
diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt
index 7b31042158..5cc49dd2f5 100644
--- a/.github/scripts/levelization/results/ordering.txt
+++ b/.github/scripts/levelization/results/ordering.txt
@@ -105,9 +105,9 @@ test.csf > xrpl.basics
 test.csf > xrpld.consensus
 test.csf > xrpl.json
 test.csf > xrpl.ledger
-test.csf > xrpl.protocol
 test.json > test.jtx
 test.json > xrpl.json
+test.jtx > test.unit_test
 test.jtx > xrpl.basics
 test.jtx > xrpl.config
 test.jtx > xrpl.core
@@ -194,8 +194,6 @@ test.shamap > xrpl.config
 test.shamap > xrpl.nodestore
 test.shamap > xrpl.protocol
 test.shamap > xrpl.shamap
-test.toplevel > test.csf
-test.toplevel > xrpl.json
 test.unit_test > xrpl.basics
 test.unit_test > xrpl.protocol
 tests.libxrpl > xrpl.basics
@@ -218,11 +216,14 @@ xrpl.core > xrpl.json
 xrpl.core > xrpl.protocol
 xrpl.json > xrpl.basics
 xrpl.ledger > xrpl.basics
+xrpl.ledger > xrpl.json
+xrpl.ledger > xrpl.nodestore
 xrpl.ledger > xrpl.protocol
 xrpl.ledger > xrpl.shamap
 xrpl.net > xrpl.basics
 xrpl.nodestore > xrpl.basics
 xrpl.nodestore > xrpl.config
+xrpl.nodestore > xrpl.json
 xrpl.nodestore > xrpl.protocol
 xrpl.protocol > xrpl.basics
 xrpl.protocol > xrpl.json
@@ -240,7 +241,6 @@ xrpl.server > xrpl.json
 xrpl.server > xrpl.protocol
 xrpl.server > xrpl.rdb
 xrpl.server > xrpl.resource
-xrpl.server > xrpl.shamap
 xrpl.shamap > xrpl.basics
 xrpl.shamap > xrpl.nodestore
 xrpl.shamap > xrpl.protocol
@@ -295,8 +295,10 @@ xrpld.peerfinder > xrpl.rdb
 xrpld.perflog > xrpl.basics
 xrpld.perflog > xrpl.config
 xrpld.perflog > xrpl.core
+xrpld.perflog > xrpld.app
 xrpld.perflog > xrpld.rpc
 xrpld.perflog > xrpl.json
+xrpld.perflog > xrpl.nodestore
 xrpld.perflog > xrpl.protocol
 xrpld.rpc > xrpl.basics
 xrpld.rpc > xrpl.config
@@ -314,5 +316,6 @@ xrpld.rpc > xrpl.shamap
 xrpld.rpc > xrpl.tx
 xrpld.shamap > xrpl.basics
 xrpld.shamap > xrpld.core
+xrpld.shamap > xrpl.nodestore
 xrpld.shamap > xrpl.protocol
 xrpld.shamap > xrpl.shamap
diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml
index 5528f3452e..a6bbe669ce 100644
--- a/.github/workflows/reusable-clang-tidy.yml
+++ b/.github/workflows/reusable-clang-tidy.yml
@@ -79,6 +79,7 @@ jobs:
               -Dtests=ON \
               -Dwerr=ON \
               -Dxrpld=ON \
+              -Dverify_headers=ON \
               ..
 
       # clang-tidy needs headers generated from proto files
@@ -91,7 +92,7 @@ jobs:
         id: run_clang_tidy
         continue-on-error: true
         env:
-          TARGETS: ${{ needs.determine-files.outputs.need_full_run != 'true' && needs.determine-files.outputs.cpp_changed_files || 'src tests' }}
+          TARGETS: ${{ needs.determine-files.outputs.need_full_run != 'true' && needs.determine-files.outputs.cpp_changed_files || 'include src tests' }}
         run: |
           set -o pipefail
           run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7bac4d1140..2e4521870d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -28,8 +28,10 @@ repos:
         entry: ./bin/pre-commit/clang_tidy_check.py
         language: python
         types_or: [c++, c]
-        exclude: ^include/xrpl/protocol_autogen
-        pass_filenames: false # script determines the staged files itself
+        # .ipp fragments are included by their owning header rather than compiled
+        # as standalone translation units, so they have no compile_commands.json
+        # entry to lint (verify_headers checks them transitively).
+        exclude: '^include/xrpl/protocol_autogen|\.ipp$'
       - id: fix-include-style
         name: fix include style
         entry: ./bin/pre-commit/fix_include_style.py
diff --git a/BUILD.md b/BUILD.md
index 847cd7bc1a..6ccdde12d5 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -317,21 +317,41 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
 
 ## Options
 
-| Option     | Default Value | Description                                                    |
-| ---------- | ------------- | -------------------------------------------------------------- |
-| `assert`   | OFF           | Force enabling assertions.                                     |
-| `coverage` | OFF           | Prepare the coverage report.                                   |
-| `tests`    | OFF           | Build tests.                                                   |
-| `unity`    | OFF           | Configure a unity build.                                       |
-| `xrpld`    | OFF           | Build the xrpld application, and not just the libxrpl library. |
-| `werr`     | OFF           | Treat compilation warnings as errors                           |
-| `wextra`   | OFF           | Enable additional compilation warnings                         |
+| Option           | Default Value | Description                                                                   |
+| ---------------- | ------------- | ----------------------------------------------------------------------------- |
+| `assert`         | OFF           | Force enabling assertions.                                                    |
+| `coverage`       | OFF           | Prepare the coverage report.                                                  |
+| `tests`          | OFF           | Build tests.                                                                  |
+| `unity`          | OFF           | Configure a unity build.                                                      |
+| `verify_headers` | ON            | Make the `verify-headers` target available to compile each header on its own. |
+| `xrpld`          | OFF           | Build the xrpld application, and not just the libxrpl library.                |
+| `werr`           | OFF           | Treat compilation warnings as errors                                          |
+| `wextra`         | OFF           | Enable additional compilation warnings                                        |
 
 [Unity builds][unity-build] may be faster for the first build (at the cost of much more
 memory) since they concatenate sources into fewer translation units. Non-unity
 builds may be faster for incremental builds, and can be helpful for detecting
 `#include` omissions.
 
+### Verifying headers
+
+The regular build only compiles `.cpp` files, so a header is only ever checked
+through whatever translation unit happens to include it. A header that forgets
+an `#include` is not caught as long as every `.cpp` that uses it includes its
+missing dependency first. The `verify_headers` option (ON by default) adds a
+`verify-headers` target that compiles every header on its own, which fails if a
+header is not self-contained:
+
+```bash
+cmake --build . --target verify-headers
+```
+
+The per-header objects are excluded from the `all` target, so a normal build
+never compiles them; they are built only through `verify-headers`. The generated
+translation units do appear in `compile_commands.json`, so clang-tidy (and
+clangd and IDEs) can lint each header on its own. Pass `-Dverify_headers=OFF` to
+omit them entirely.
+
 ## Troubleshooting
 
 ### Conan
diff --git a/bin/pre-commit/clang_tidy_check.py b/bin/pre-commit/clang_tidy_check.py
index f134660671..d074c56acf 100755
--- a/bin/pre-commit/clang_tidy_check.py
+++ b/bin/pre-commit/clang_tidy_check.py
@@ -1,21 +1,22 @@
 #!/usr/bin/env python3
-"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy."""
+"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy.
+
+The set of files is chosen by pre-commit (see .pre-commit-config.yaml), which
+filters to C/C++ sources and excludes `.ipp` fragments. Headers are linted
+directly: the `verify_headers` build option (ON by default) compiles every
+`.h`/`.hpp` on its own, so each header is the main file of its own
+compile_commands.json entry and run-clang-tidy can analyse it just like a
+`.cpp`.
+"""
 
 from __future__ import annotations
 
-import json
 import os
-import re
 import shutil
 import subprocess
 import sys
-from collections import defaultdict
 from pathlib import Path
 
-HEADER_EXTENSIONS = {".h", ".hpp", ".ipp"}
-SOURCE_EXTENSIONS = {".cpp"}
-INCLUDE_RE = re.compile(r"^\s*#\s*include\s*[<\"]([^>\"]+)[>\"]")
-
 
 def find_run_clang_tidy() -> str | None:
     for candidate in ("run-clang-tidy-21", "run-clang-tidy"):
@@ -32,150 +33,11 @@ def find_build_dir(repo_root: Path) -> Path | None:
     return None
 
 
-def build_include_graph(build_dir: Path, repo_root: Path) -> tuple[dict, set]:
-    """
-    Scan all files reachable from compile_commands.json and build an inverted include graph.
-
-    Returns:
-        inverted: header_path -> set of files that include it
-        source_files: set of all TU paths from compile_commands.json
-    """
-    with open(build_dir / "compile_commands.json") as f:
-        db = json.load(f)
-
-    source_files = {Path(e["file"]).resolve() for e in db}
-    include_roots = [repo_root / "include", repo_root / "src"]
-    inverted: dict[Path, set[Path]] = defaultdict(set)
-
-    to_scan: set[Path] = set(source_files)
-    scanned: set[Path] = set()
-
-    while to_scan:
-        file = to_scan.pop()
-        if file in scanned or not file.exists():
-            continue
-        scanned.add(file)
-
-        content = file.read_text()
-
-        for line in content.splitlines():
-            m = INCLUDE_RE.match(line)
-            if not m:
-                continue
-            for root in include_roots:
-                candidate = (root / m.group(1)).resolve()
-                if candidate.exists():
-                    inverted[candidate].add(file)
-                    if candidate not in scanned:
-                        to_scan.add(candidate)
-                    break
-
-    return inverted, source_files
-
-
-def find_tus_for_headers(
-    headers: list[Path],
-    inverted: dict[Path, set[Path]],
-    source_files: set[Path],
-) -> set[Path]:
-    """
-    For each header, pick one TU that transitively includes it.
-    Prefers a TU whose stem matches the header's stem, otherwise picks the first found.
-    """
-    result: set[Path] = set()
-
-    for header in headers:
-        preferred: Path | None = None
-        visited: set[Path] = {header}
-        stack: list[Path] = [header]
-
-        while stack:
-            h = stack.pop()
-            for inc in inverted.get(h, ()):
-                if inc in source_files:
-                    if inc.stem == header.stem:
-                        preferred = inc
-                        break
-                    if preferred is None:
-                        preferred = inc
-                if inc not in visited:
-                    visited.add(inc)
-                    stack.append(inc)
-            if preferred is not None and preferred.stem == header.stem:
-                break
-
-        if preferred is not None:
-            result.add(preferred)
-
-    return result
-
-
-def resolve_files(
-    input_files: list[str], build_dir: Path, repo_root: Path
-) -> list[str]:
-    """
-    Split input into source files and headers. Source files are passed through;
-    headers are resolved to the TUs that transitively include them.
-    """
-    sources: list[Path] = []
-    headers: list[Path] = []
-
-    for f in input_files:
-        p = Path(f).resolve()
-        if p.suffix in SOURCE_EXTENSIONS:
-            sources.append(p)
-        elif p.suffix in HEADER_EXTENSIONS:
-            headers.append(p)
-
-    if not headers:
-        return [str(p) for p in sources]
-
-    print(
-        f"Resolving {len(headers)} header(s) to compilation units...", file=sys.stderr
-    )
-    inverted, source_files = build_include_graph(build_dir, repo_root)
-    tus = find_tus_for_headers(headers, inverted, source_files)
-
-    if not tus:
-        print(
-            "Warning: no compilation units found that include the modified headers; "
-            "skipping clang-tidy for headers.",
-            file=sys.stderr,
-        )
-
-    return sorted({str(p) for p in (*sources, *tus)})
-
-
-def staged_files(repo_root: Path) -> list[str]:
-    result = subprocess.run(
-        ["git", "diff", "--staged", "--name-only", "--diff-filter=d"],
-        capture_output=True,
-        text=True,
-        cwd=repo_root,
-    )
-    if result.returncode != 0:
-        print(
-            "clang-tidy check failed: 'git diff --staged' command failed.",
-            file=sys.stderr,
-        )
-        if result.stderr:
-            print(result.stderr, file=sys.stderr)
-        sys.exit(result.returncode or 1)
-    return [str(repo_root / p) for p in result.stdout.splitlines() if p]
-
-
 def main():
     if not os.environ.get("TIDY"):
         return 0
 
-    repo_root = Path(
-        subprocess.check_output(
-            ["git", "rev-parse", "--show-toplevel"],
-            cwd=Path(__file__).parent,
-            text=True,
-        ).strip()
-    )
-    files = staged_files(repo_root)
+    files = sys.argv[1:]
     if not files:
         return 0
 
@@ -188,6 +50,13 @@ def main():
         )
         return 1
 
+    repo_root = Path(
+        subprocess.check_output(
+            ["git", "rev-parse", "--show-toplevel"],
+            cwd=Path(__file__).parent,
+            text=True,
+        ).strip()
+    )
     build_dir = find_build_dir(repo_root)
     if not build_dir:
         print(
@@ -197,13 +66,9 @@ def main():
         )
         return 1
 
-    tidy_files = resolve_files(files, build_dir, repo_root)
-    if not tidy_files:
-        return 0
-
     result = subprocess.run(
         [run_clang_tidy, "-quiet", "-p", str(build_dir), "-fix", "-allow-no-checks"]
-        + tidy_files
+        + files
     )
     return result.returncode
 
diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake
index 4d4a800d9a..3e49267715 100644
--- a/cmake/XrplCore.cmake
+++ b/cmake/XrplCore.cmake
@@ -293,4 +293,13 @@ if(xrpld)
             PRIVATE ${CMAKE_SOURCE_DIR}/external/antithesis-sdk
         )
     endif()
+
+    # The xrpld headers are not built with add_module, so verify them against
+    # the executable's own compile environment.
+    if(verify_headers)
+        verify_target_headers(xrpld "${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld")
+        if(tests)
+            verify_target_headers(xrpld "${CMAKE_CURRENT_SOURCE_DIR}/src/test")
+        endif()
+    endif()
 endif()
diff --git a/cmake/XrplSettings.cmake b/cmake/XrplSettings.cmake
index 44a727a994..757a596096 100644
--- a/cmake/XrplSettings.cmake
+++ b/cmake/XrplSettings.cmake
@@ -30,6 +30,23 @@ if(tests)
     endif()
 endif()
 
+# Enabled by default so every header is compiled on its own as the main file of
+# its own compile_commands.json entry - this is what lets clang-tidy (and clangd
+# and IDEs) analyse a header's own includes directly. The per-header objects are
+# EXCLUDE_FROM_ALL (see cmake/verify_headers.cmake) and the aggregate target
+# below is not part of `all`, so a normal `cmake --build` never compiles them.
+option(
+    verify_headers
+    "Compile every header on its own to verify it is self-contained."
+    ON
+)
+if(verify_headers)
+    # Aggregate target that builds every per-module header-verification library
+    # created by add_module (see cmake/verify_headers.cmake). Build it with:
+    #   cmake --build . --target verify-headers
+    add_custom_target(verify-headers)
+endif()
+
 option(unity "Creates a build using UNITY support in cmake." OFF)
 if(unity)
     if(NOT is_ci)
diff --git a/cmake/add_module.cmake b/cmake/add_module.cmake
index 316d6c627b..b72d1077bb 100644
--- a/cmake/add_module.cmake
+++ b/cmake/add_module.cmake
@@ -1,4 +1,5 @@
 include(isolate_headers)
+include(verify_headers)
 
 # Create an OBJECT library target named
 #
@@ -37,4 +38,20 @@ function(add_module parent name)
         "${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}"
         PRIVATE
     )
+    # protocol_autogen contains generated headers that are deliberately exempt
+    # from clang-tidy (see ExcludeHeaderFilterRegex in .clang-tidy), so we do not
+    # verify them either.
+    if(
+        verify_headers
+        AND NOT "${parent}/${name}" STREQUAL "xrpl/protocol_autogen"
+    )
+        verify_target_headers(
+            ${target}
+            "${CMAKE_CURRENT_SOURCE_DIR}/include/${parent}/${name}"
+        )
+        verify_target_headers(
+            ${target}
+            "${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}"
+        )
+    endif()
 endfunction()
diff --git a/cmake/verify_headers.cmake b/cmake/verify_headers.cmake
new file mode 100644
index 0000000000..2c36869441
--- /dev/null
+++ b/cmake/verify_headers.cmake
@@ -0,0 +1,84 @@
+# Our normal build only ever compiles `.cpp` files, so a header is only ever
+# checked through whatever translation unit happens to include it. A header that
+# is missing an `#include` is never caught as long as every `.cpp` that uses it
+# includes its missing dependency first. To check a header on its own we compile
+# it directly as a translation unit.
+#
+# Compiling the header itself - rather than a `.cpp` wrapper that includes it -
+# gives two checks at once:
+#   * the compiler fails if the header is not self-contained, i.e. it uses a
+#     declaration that is not available (directly or transitively); and
+#   * the header is the *main file* of its `compile_commands.json` entry, so
+#     clang-tidy's misc-include-cleaner analyses (and can --fix) the header's own
+#     includes - flagging a dependency that is only available transitively, which
+#     a plain compile cannot catch. A wrapper would be the main file instead, and
+#     include-cleaner never looks inside the headers a main file includes.
+#
+# The objects are never linked anywhere; we build them only for these checks.
+
+# Verify that the headers under headers_dir compile on their own, using the
+# compile environment of an existing target so each header is compiled exactly as
+# that target compiles it. This works for both add_module libraries and the xrpld
+# and test binaries: a library's isolated public and private include directories
+# and a binary's `-I src` both live in its INCLUDE_DIRECTORIES, and the modules or
+# libraries it links live in its LINK_LIBRARIES. We copy those usage requirements
+# through generator expressions (rather than linking ${target}, which is
+# impossible for an executable), evaluated at generation time so they capture
+# requirements the caller adds after this runs. The verify library is created
+# once; call this repeatedly to add more header directories.
+#
+# verify_target_headers(target headers_dir)
+function(verify_target_headers target headers_dir)
+    set(verify ${target}.verify)
+    if(NOT TARGET ${verify})
+        add_library(${verify} OBJECT EXCLUDE_FROM_ALL)
+        # A unity build would concatenate the headers into a single translation
+        # unit, where a header missing an include could be satisfied by one that
+        # precedes it in the blob - exactly the bug we want to catch.
+        set_target_properties(${verify} PROPERTIES UNITY_BUILD OFF)
+        target_include_directories(
+            ${verify}
+            PRIVATE $
+        )
+        target_compile_definitions(
+            ${verify}
+            PRIVATE $
+        )
+        target_compile_options(
+            ${verify}
+            PRIVATE $
+        )
+        target_link_libraries(
+            ${verify}
+            PRIVATE $
+        )
+        add_dependencies(verify-headers ${verify})
+    endif()
+    _verify_add_headers(${verify} "${headers_dir}")
+endfunction()
+
+# Add every .h/.hpp under dir to target as a directly-compiled C++ translation
+# unit. .ipp files are inline-implementation fragments included by their owning
+# header (often after a class declaration), so they are not self-contained on
+# their own and are verified transitively when that header is verified.
+function(_verify_add_headers target dir)
+    file(GLOB_RECURSE headers CONFIGURE_DEPENDS "${dir}/*.h" "${dir}/*.hpp")
+    if(NOT headers)
+        return()
+    endif()
+    # `-xc++` forces the header to be compiled as a C++ translation unit; a lone
+    # `.h` is otherwise treated as a header to precompile. `#pragma once` is
+    # harmless (and warns) when the header is the main file, so silence it.
+    # Compiled on its own, a header legitimately defines constants and static or
+    # template functions that nothing in this single translation unit uses (they
+    # exist for the files that include it), so the resulting unused-entity
+    # warnings are expected and must not fail the build under -Werror.
+    set_source_files_properties(
+        ${headers}
+        PROPERTIES
+            LANGUAGE CXX
+            COMPILE_OPTIONS
+                "-xc++;-Wno-pragma-once-outside-header;-Wno-unused-const-variable;-Wno-unused-function"
+    )
+    target_sources(${target} PRIVATE ${headers})
+endfunction()
diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h
index 59968a4fa4..c0ae8ef56e 100644
--- a/include/xrpl/basics/Buffer.h
+++ b/include/xrpl/basics/Buffer.h
@@ -6,6 +6,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/CompressionAlgorithms.h b/include/xrpl/basics/CompressionAlgorithms.h
index e24c490337..a5ec8645b6 100644
--- a/include/xrpl/basics/CompressionAlgorithms.h
+++ b/include/xrpl/basics/CompressionAlgorithms.h
@@ -5,6 +5,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/DecayingSample.h b/include/xrpl/basics/DecayingSample.h
index 910c8f9e14..86a8baa62e 100644
--- a/include/xrpl/basics/DecayingSample.h
+++ b/include/xrpl/basics/DecayingSample.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/FileUtilities.h b/include/xrpl/basics/FileUtilities.h
index 8cf7e4893f..c7a427b8a9 100644
--- a/include/xrpl/basics/FileUtilities.h
+++ b/include/xrpl/basics/FileUtilities.h
@@ -3,7 +3,9 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/IntrusivePointer.h b/include/xrpl/basics/IntrusivePointer.h
index d66c340d3f..c23d6afb85 100644
--- a/include/xrpl/basics/IntrusivePointer.h
+++ b/include/xrpl/basics/IntrusivePointer.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/IntrusiveRefCounts.h b/include/xrpl/basics/IntrusiveRefCounts.h
index 0b00f1d5b1..5eb1422541 100644
--- a/include/xrpl/basics/IntrusiveRefCounts.h
+++ b/include/xrpl/basics/IntrusiveRefCounts.h
@@ -3,6 +3,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/basics/Log.h b/include/xrpl/basics/Log.h
index 0699cdd3d9..4e3437fe71 100644
--- a/include/xrpl/basics/Log.h
+++ b/include/xrpl/basics/Log.h
@@ -1,6 +1,5 @@
 #pragma once
 
-#include 
 #include 
 
 #include 
@@ -11,7 +10,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h
index cee0c45355..073da12f89 100644
--- a/include/xrpl/basics/Number.h
+++ b/include/xrpl/basics/Number.h
@@ -2,7 +2,9 @@
 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -11,7 +13,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/RangeSet.h b/include/xrpl/basics/RangeSet.h
index e1cee8b6c4..2ed543b376 100644
--- a/include/xrpl/basics/RangeSet.h
+++ b/include/xrpl/basics/RangeSet.h
@@ -6,6 +6,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/Resolver.h b/include/xrpl/basics/Resolver.h
index 3b6a950247..d48958b76d 100644
--- a/include/xrpl/basics/Resolver.h
+++ b/include/xrpl/basics/Resolver.h
@@ -3,6 +3,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/basics/ResolverAsio.h b/include/xrpl/basics/ResolverAsio.h
index 2764777327..0b78b9747c 100644
--- a/include/xrpl/basics/ResolverAsio.h
+++ b/include/xrpl/basics/ResolverAsio.h
@@ -5,6 +5,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class ResolverAsio : public Resolver
diff --git a/include/xrpl/basics/SHAMapHash.h b/include/xrpl/basics/SHAMapHash.h
index 76d9d4fa3d..3c3d525022 100644
--- a/include/xrpl/basics/SHAMapHash.h
+++ b/include/xrpl/basics/SHAMapHash.h
@@ -3,7 +3,9 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/SharedWeakCachePointer.h b/include/xrpl/basics/SharedWeakCachePointer.h
index c2c3239eea..a143647a1e 100644
--- a/include/xrpl/basics/SharedWeakCachePointer.h
+++ b/include/xrpl/basics/SharedWeakCachePointer.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/basics/SlabAllocator.h b/include/xrpl/basics/SlabAllocator.h
index 0172b1ade2..8e741991f6 100644
--- a/include/xrpl/basics/SlabAllocator.h
+++ b/include/xrpl/basics/SlabAllocator.h
@@ -15,6 +15,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #if BOOST_OS_LINUX
diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h
index 1d3434b7ed..97df43d68f 100644
--- a/include/xrpl/basics/StringUtilities.h
+++ b/include/xrpl/basics/StringUtilities.h
@@ -1,18 +1,19 @@
 #pragma once
 
 #include 
-#include 
 
 #include 
 #include 
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h
index 973fcd828a..71ebfc57a4 100644
--- a/include/xrpl/basics/TaggedCache.h
+++ b/include/xrpl/basics/TaggedCache.h
@@ -1,17 +1,24 @@
 #pragma once
 
-#include 
-#include 
-#include 
+#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp
index 7e812ce4c7..ffcd533216 100644
--- a/include/xrpl/basics/TaggedCache.ipp
+++ b/include/xrpl/basics/TaggedCache.ipp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include   // IWYU pragma: keep
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/basics/UnorderedContainers.h b/include/xrpl/basics/UnorderedContainers.h
index 5a417d5045..e0700c4055 100644
--- a/include/xrpl/basics/UnorderedContainers.h
+++ b/include/xrpl/basics/UnorderedContainers.h
@@ -2,12 +2,14 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
 
 /**
  * Use hash_* containers for keys that do not need a cryptographically secure
diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h
index 660958ce14..24fd660e65 100644
--- a/include/xrpl/basics/base64.h
+++ b/include/xrpl/basics/base64.h
@@ -34,6 +34,7 @@
 
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h
index 93520ff699..e6ca1993f9 100644
--- a/include/xrpl/basics/base_uint.h
+++ b/include/xrpl/basics/base_uint.h
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -18,8 +19,17 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/basics/chrono.h b/include/xrpl/basics/chrono.h
index 5d6de06248..61246fc699 100644
--- a/include/xrpl/basics/chrono.h
+++ b/include/xrpl/basics/chrono.h
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/hardened_hash.h b/include/xrpl/basics/hardened_hash.h
index b8ea1e0f3f..5a855736b3 100644
--- a/include/xrpl/basics/hardened_hash.h
+++ b/include/xrpl/basics/hardened_hash.h
@@ -1,6 +1,5 @@
 #pragma once
 
-#include 
 #include 
 
 #include 
diff --git a/include/xrpl/basics/join.h b/include/xrpl/basics/join.h
index c214212473..492e4f3122 100644
--- a/include/xrpl/basics/join.h
+++ b/include/xrpl/basics/join.h
@@ -1,7 +1,9 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/basics/make_SSLContext.h b/include/xrpl/basics/make_SSLContext.h
index 46f6a15e84..45ac637c36 100644
--- a/include/xrpl/basics/make_SSLContext.h
+++ b/include/xrpl/basics/make_SSLContext.h
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h
index c51cedf2dd..c2750a5769 100644
--- a/include/xrpl/basics/partitioned_unordered_map.h
+++ b/include/xrpl/basics/partitioned_unordered_map.h
@@ -3,7 +3,10 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h
index 0b298e12d9..cceaa6f029 100644
--- a/include/xrpl/basics/random.h
+++ b/include/xrpl/basics/random.h
@@ -3,7 +3,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/basics/safe_cast.h b/include/xrpl/basics/safe_cast.h
index f71edc47ad..714146e089 100644
--- a/include/xrpl/basics/safe_cast.h
+++ b/include/xrpl/basics/safe_cast.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/basics/strHex.h b/include/xrpl/basics/strHex.h
index 9cae234f06..1366515bd3 100644
--- a/include/xrpl/basics/strHex.h
+++ b/include/xrpl/basics/strHex.h
@@ -3,6 +3,9 @@
 #include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 template 
diff --git a/include/xrpl/basics/tagged_integer.h b/include/xrpl/basics/tagged_integer.h
index ddcde479f3..8fbd2a274b 100644
--- a/include/xrpl/basics/tagged_integer.h
+++ b/include/xrpl/basics/tagged_integer.h
@@ -7,6 +7,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/beast/asio/io_latency_probe.h b/include/xrpl/beast/asio/io_latency_probe.h
index 5e1b098dcb..f67ff4a692 100644
--- a/include/xrpl/beast/asio/io_latency_probe.h
+++ b/include/xrpl/beast/asio/io_latency_probe.h
@@ -8,6 +8,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/beast/container/aged_container_utility.h b/include/xrpl/beast/container/aged_container_utility.h
index 879672e9cf..7cda863fab 100644
--- a/include/xrpl/beast/container/aged_container_utility.h
+++ b/include/xrpl/beast/container/aged_container_utility.h
@@ -3,6 +3,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace beast {
diff --git a/include/xrpl/beast/container/aged_map.h b/include/xrpl/beast/container/aged_map.h
index c1f6943451..20daab70a4 100644
--- a/include/xrpl/beast/container/aged_map.h
+++ b/include/xrpl/beast/container/aged_map.h
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/container/aged_multimap.h b/include/xrpl/beast/container/aged_multimap.h
index 65efd1bbf9..f6133ced1c 100644
--- a/include/xrpl/beast/container/aged_multimap.h
+++ b/include/xrpl/beast/container/aged_multimap.h
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/container/aged_unordered_map.h b/include/xrpl/beast/container/aged_unordered_map.h
index a2189e2409..d6ea6e97bd 100644
--- a/include/xrpl/beast/container/aged_unordered_map.h
+++ b/include/xrpl/beast/container/aged_unordered_map.h
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/container/aged_unordered_multimap.h b/include/xrpl/beast/container/aged_unordered_multimap.h
index f1348ed39f..3b72be98b7 100644
--- a/include/xrpl/beast/container/aged_unordered_multimap.h
+++ b/include/xrpl/beast/container/aged_unordered_multimap.h
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h
index 4cb2246a22..0533a51f00 100644
--- a/include/xrpl/beast/container/detail/aged_ordered_container.h
+++ b/include/xrpl/beast/container/detail/aged_ordered_container.h
@@ -11,9 +11,14 @@
 #include 
 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
@@ -1247,12 +1252,7 @@ AgedOrderedContainer::AgedOrd
 template 
 AgedOrderedContainer::AgedOrderedContainer(
     AgedOrderedContainer const& other)
-    : config_(other.config_)
-#if BOOST_VERSION >= 108000
-    , cont_(other.cont_.get_comp())
-#else
-    , cont_(other.cont_.comp())
-#endif
+    : config_(other.config_), cont_(other.cont_.get_comp())
 {
     insert(other.cbegin(), other.cend());
 }
@@ -1261,12 +1261,7 @@ template ::AgedOrderedContainer(
     AgedOrderedContainer const& other,
     Allocator const& alloc)
-    : config_(other.config_, alloc)
-#if BOOST_VERSION >= 108000
-    , cont_(other.cont_.get_comp())
-#else
-    , cont_(other.cont_.comp())
-#endif
+    : config_(other.config_, alloc), cont_(other.cont_.get_comp())
 {
     insert(other.cbegin(), other.cend());
 }
@@ -1283,13 +1278,7 @@ template ::AgedOrderedContainer(
     AgedOrderedContainer&& other,  // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
     Allocator const& alloc)
-    : config_(std::move(other.config_), alloc)
-#if BOOST_VERSION >= 108000
-    , cont_(std::move(other.cont_.get_comp()))
-#else
-    , cont_(std::move(other.cont_.comp()))
-#endif
-
+    : config_(std::move(other.config_), alloc), cont_(std::move(other.cont_.get_comp()))
 {
     insert(other.cbegin(), other.cend());
     other.clear();
diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h
index 3bad12d9e5..782f36cd52 100644
--- a/include/xrpl/beast/container/detail/aged_unordered_container.h
+++ b/include/xrpl/beast/container/detail/aged_unordered_container.h
@@ -10,13 +10,19 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
+#include 
 
 /*
 
diff --git a/include/xrpl/beast/core/CurrentThreadName.h b/include/xrpl/beast/core/CurrentThreadName.h
index 6175d99b16..3cdfe4c678 100644
--- a/include/xrpl/beast/core/CurrentThreadName.h
+++ b/include/xrpl/beast/core/CurrentThreadName.h
@@ -6,6 +6,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h
index 18e63c9c10..8faf90f53d 100644
--- a/include/xrpl/beast/core/LexicalCast.h
+++ b/include/xrpl/beast/core/LexicalCast.h
@@ -5,11 +5,12 @@
 #include 
 
 #include 
-#include 
+#include 
 #include 
-#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h
index 1c3827ae1c..1eeeaa87d1 100644
--- a/include/xrpl/beast/core/List.h
+++ b/include/xrpl/beast/core/List.h
@@ -1,6 +1,8 @@
 #pragma once
 
+#include 
 #include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/core/LockFreeStack.h b/include/xrpl/beast/core/LockFreeStack.h
index d4ad45cf5c..19225a4343 100644
--- a/include/xrpl/beast/core/LockFreeStack.h
+++ b/include/xrpl/beast/core/LockFreeStack.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 
@@ -58,7 +59,7 @@ public:
         return result;
     }
 
-    NodePtr
+    [[nodiscard]] NodePtr
     node() const
     {
         return node_;
diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h
index 83cff4bdea..0b36d4c983 100644
--- a/include/xrpl/beast/hash/hash_append.h
+++ b/include/xrpl/beast/hash/hash_append.h
@@ -5,8 +5,8 @@
 
 #include 
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/beast/insight/Collector.h b/include/xrpl/beast/insight/Collector.h
index 2e73d60400..3f83e329d4 100644
--- a/include/xrpl/beast/insight/Collector.h
+++ b/include/xrpl/beast/insight/Collector.h
@@ -4,8 +4,10 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
+#include 
 #include 
 
 namespace beast::insight {
diff --git a/include/xrpl/beast/insight/Insight.h b/include/xrpl/beast/insight/Insight.h
deleted file mode 100644
index bf3743cfd8..0000000000
--- a/include/xrpl/beast/insight/Insight.h
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
diff --git a/include/xrpl/beast/insight/NullCollector.h b/include/xrpl/beast/insight/NullCollector.h
index b865526ade..67903420fa 100644
--- a/include/xrpl/beast/insight/NullCollector.h
+++ b/include/xrpl/beast/insight/NullCollector.h
@@ -2,6 +2,8 @@
 
 #include 
 
+#include 
+
 namespace beast::insight {
 
 /** A Collector which does not collect metrics. */
diff --git a/include/xrpl/beast/insight/StatsDCollector.h b/include/xrpl/beast/insight/StatsDCollector.h
index ad436dc626..9a438c48f1 100644
--- a/include/xrpl/beast/insight/StatsDCollector.h
+++ b/include/xrpl/beast/insight/StatsDCollector.h
@@ -4,6 +4,9 @@
 #include 
 #include 
 
+#include 
+#include 
+
 namespace beast::insight {
 
 /** A Collector that reports metrics to a StatsD server.
diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h
index 67deaaa787..f4327b7b8a 100644
--- a/include/xrpl/beast/net/IPAddress.h
+++ b/include/xrpl/beast/net/IPAddress.h
@@ -9,6 +9,7 @@
 #include 
 #include 
 
+#include 
 #include 
 
 //------------------------------------------------------------------------------
diff --git a/include/xrpl/beast/net/IPAddressV4.h b/include/xrpl/beast/net/IPAddressV4.h
index dbe5a6095f..9367fbe1eb 100644
--- a/include/xrpl/beast/net/IPAddressV4.h
+++ b/include/xrpl/beast/net/IPAddressV4.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-
 #include 
 
 namespace beast::IP {
diff --git a/include/xrpl/beast/net/IPAddressV6.h b/include/xrpl/beast/net/IPAddressV6.h
index 10f806417d..1bfa079990 100644
--- a/include/xrpl/beast/net/IPAddressV6.h
+++ b/include/xrpl/beast/net/IPAddressV6.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-
 #include 
 
 namespace beast::IP {
diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h
index fec6e1556f..0b661108f2 100644
--- a/include/xrpl/beast/net/IPEndpoint.h
+++ b/include/xrpl/beast/net/IPEndpoint.h
@@ -3,8 +3,13 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h
index e810733210..7c681ab140 100644
--- a/include/xrpl/beast/rfc2616.h
+++ b/include/xrpl/beast/rfc2616.h
@@ -8,6 +8,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/beast/test/yield_to.h b/include/xrpl/beast/test/yield_to.h
index 84d7d8846d..1a34ec436e 100644
--- a/include/xrpl/beast/test/yield_to.h
+++ b/include/xrpl/beast/test/yield_to.h
@@ -11,6 +11,8 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/beast/unit_test.h b/include/xrpl/beast/unit_test.h
index 51ac96cacb..b4d53b2b1c 100644
--- a/include/xrpl/beast/unit_test.h
+++ b/include/xrpl/beast/unit_test.h
@@ -1,15 +1,6 @@
 #pragma once
 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
 #include 
-#include 
-#include 
 
 #ifndef BEAST_EXPECT
 #define BEAST_EXPECT_S1(x) #x
diff --git a/include/xrpl/beast/unit_test/match.h b/include/xrpl/beast/unit_test/match.h
index da466ab228..5faeaa1100 100644
--- a/include/xrpl/beast/unit_test/match.h
+++ b/include/xrpl/beast/unit_test/match.h
@@ -7,6 +7,7 @@
 #include 
 
 #include 
+#include 
 
 namespace beast::unit_test {
 
diff --git a/include/xrpl/beast/unit_test/recorder.h b/include/xrpl/beast/unit_test/recorder.h
index 2ed88d4a46..1b7347dc2e 100644
--- a/include/xrpl/beast/unit_test/recorder.h
+++ b/include/xrpl/beast/unit_test/recorder.h
@@ -6,6 +6,10 @@
 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace beast::unit_test {
 
diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h
index ff990dece5..a903d9f8c2 100644
--- a/include/xrpl/beast/unit_test/reporter.h
+++ b/include/xrpl/beast/unit_test/reporter.h
@@ -5,18 +5,21 @@
 #pragma once
 
 #include 
-#include 
+#include 
+#include 
 
 #include 
 #include 
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 namespace beast::unit_test {
 
diff --git a/include/xrpl/beast/unit_test/results.h b/include/xrpl/beast/unit_test/results.h
index 02aa9730d1..718d764c9f 100644
--- a/include/xrpl/beast/unit_test/results.h
+++ b/include/xrpl/beast/unit_test/results.h
@@ -6,6 +6,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h
index fded866da0..487663fcc5 100644
--- a/include/xrpl/beast/unit_test/suite.h
+++ b/include/xrpl/beast/unit_test/suite.h
@@ -10,6 +10,8 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -634,7 +636,7 @@ Suite::run(Runner& r)
 #define BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Class, Module, Library, Priority)
 
 #else
-#include 
+#include   // IWYU pragma: keep
 #define BEAST_DEFINE_TESTSUITE(Class, Module, Library) \
     BEAST_DEFINE_TESTSUITE_INSERT(Class, Module, Library, false, 0)
 #define BEAST_DEFINE_TESTSUITE_MANUAL(Class, Module, Library) \
diff --git a/include/xrpl/beast/unit_test/suite_info.h b/include/xrpl/beast/unit_test/suite_info.h
index c09a0c2257..bda10ae7e3 100644
--- a/include/xrpl/beast/unit_test/suite_info.h
+++ b/include/xrpl/beast/unit_test/suite_info.h
@@ -4,9 +4,9 @@
 
 #pragma once
 
-#include 
 #include 
 #include 
+#include 
 #include 
 
 namespace beast::unit_test {
diff --git a/include/xrpl/beast/unit_test/suite_list.h b/include/xrpl/beast/unit_test/suite_list.h
index 748f994602..cf9fb9c5b1 100644
--- a/include/xrpl/beast/unit_test/suite_list.h
+++ b/include/xrpl/beast/unit_test/suite_list.h
@@ -10,6 +10,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/beast/unit_test/thread.h b/include/xrpl/beast/unit_test/thread.h
index 7ae093eb85..0de039cb89 100644
--- a/include/xrpl/beast/unit_test/thread.h
+++ b/include/xrpl/beast/unit_test/thread.h
@@ -6,7 +6,9 @@
 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/beast/utility/Journal.h b/include/xrpl/beast/utility/Journal.h
index 1262a64179..ac08b1384b 100644
--- a/include/xrpl/beast/utility/Journal.h
+++ b/include/xrpl/beast/utility/Journal.h
@@ -3,7 +3,10 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/utility/PropertyStream.h b/include/xrpl/beast/utility/PropertyStream.h
index 62de019edd..3fb6df53d9 100644
--- a/include/xrpl/beast/utility/PropertyStream.h
+++ b/include/xrpl/beast/utility/PropertyStream.h
@@ -3,8 +3,10 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace beast {
 
diff --git a/include/xrpl/beast/utility/WrappedSink.h b/include/xrpl/beast/utility/WrappedSink.h
index 22d75927fe..a24ad595db 100644
--- a/include/xrpl/beast/utility/WrappedSink.h
+++ b/include/xrpl/beast/utility/WrappedSink.h
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 
 namespace beast {
diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h
index 2ea84a7a3d..0fc3ffe0d0 100644
--- a/include/xrpl/beast/utility/rngfill.h
+++ b/include/xrpl/beast/utility/rngfill.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-
 #include 
 #include 
 #include 
diff --git a/include/xrpl/conditions/Condition.h b/include/xrpl/conditions/Condition.h
index 66d1d24736..3c798663d7 100644
--- a/include/xrpl/conditions/Condition.h
+++ b/include/xrpl/conditions/Condition.h
@@ -4,8 +4,12 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl::cryptoconditions {
 
diff --git a/include/xrpl/conditions/Fulfillment.h b/include/xrpl/conditions/Fulfillment.h
index fd8cd7d31e..a3001b2620 100644
--- a/include/xrpl/conditions/Fulfillment.h
+++ b/include/xrpl/conditions/Fulfillment.h
@@ -4,6 +4,11 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::cryptoconditions {
 
 struct Fulfillment
diff --git a/include/xrpl/conditions/detail/PreimageSha256.h b/include/xrpl/conditions/detail/PreimageSha256.h
index c592ea37ee..0973a52e4a 100644
--- a/include/xrpl/conditions/detail/PreimageSha256.h
+++ b/include/xrpl/conditions/detail/PreimageSha256.h
@@ -7,7 +7,11 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl::cryptoconditions {
 
diff --git a/include/xrpl/conditions/detail/utils.h b/include/xrpl/conditions/detail/utils.h
index 87f2265034..bf16bfb42b 100644
--- a/include/xrpl/conditions/detail/utils.h
+++ b/include/xrpl/conditions/detail/utils.h
@@ -6,7 +6,10 @@
 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 
 // A collection of functions to decode binary blobs
 // encoded with X.690 Distinguished Encoding Rules.
diff --git a/include/xrpl/config/BasicConfig.h b/include/xrpl/config/BasicConfig.h
index 858bf8bf2e..5680b51fe7 100644
--- a/include/xrpl/config/BasicConfig.h
+++ b/include/xrpl/config/BasicConfig.h
@@ -6,9 +6,13 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/core/ClosureCounter.h b/include/xrpl/core/ClosureCounter.h
index fb13047f40..ed15db032e 100644
--- a/include/xrpl/core/ClosureCounter.h
+++ b/include/xrpl/core/ClosureCounter.h
@@ -1,11 +1,14 @@
 #pragma once
 
 #include 
+#include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/core/HashRouter.h b/include/xrpl/core/HashRouter.h
index c8b34d8e93..ef8f24d43c 100644
--- a/include/xrpl/core/HashRouter.h
+++ b/include/xrpl/core/HashRouter.h
@@ -4,10 +4,16 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/core/Job.h b/include/xrpl/core/Job.h
index 6af32eb2d8..e16d7412bf 100644
--- a/include/xrpl/core/Job.h
+++ b/include/xrpl/core/Job.h
@@ -2,9 +2,14 @@
 
 #include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/core/JobQueue.h b/include/xrpl/core/JobQueue.h
index fc15e9a064..14170a39be 100644
--- a/include/xrpl/core/JobQueue.h
+++ b/include/xrpl/core/JobQueue.h
@@ -3,7 +3,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 
@@ -12,10 +11,27 @@
 // `boost/context/pooled_fixedsize_stack.hpp`, whose `.malloc()` / `.free()`
 // member calls on `boost::pool` collide with MSVC's `_CRTDBG_MAP_ALLOC` macros
 // in Debug builds (see cmake/XrplCompiler.cmake).
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
@@ -112,7 +128,7 @@ public:
         resume();
 
         /** Returns true if the Coro is still runnable (has not returned). */
-        bool
+        [[nodiscard]] bool
         runnable() const;
 
         /** Once called, the Coro allows early exit without an assert. */
@@ -384,7 +400,7 @@ private:
 
 }  // namespace xrpl
 
-#include 
+#include   // IWYU pragma: keep
 
 namespace xrpl {
 
diff --git a/include/xrpl/core/JobTypeData.h b/include/xrpl/core/JobTypeData.h
index 4e9f95dc04..d53440e1ca 100644
--- a/include/xrpl/core/JobTypeData.h
+++ b/include/xrpl/core/JobTypeData.h
@@ -2,7 +2,10 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 #include 
 
diff --git a/include/xrpl/core/JobTypeInfo.h b/include/xrpl/core/JobTypeInfo.h
index 430e80b388..b5db0dbaab 100644
--- a/include/xrpl/core/JobTypeInfo.h
+++ b/include/xrpl/core/JobTypeInfo.h
@@ -2,6 +2,10 @@
 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Holds all the 'static' information about a job, which does not change */
diff --git a/include/xrpl/core/JobTypes.h b/include/xrpl/core/JobTypes.h
index fb5c7988cb..f338b19f6c 100644
--- a/include/xrpl/core/JobTypes.h
+++ b/include/xrpl/core/JobTypes.h
@@ -1,10 +1,14 @@
 #pragma once
 
+#include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/core/LoadMonitor.h b/include/xrpl/core/LoadMonitor.h
index 32a813baa7..f1a8eb6c56 100644
--- a/include/xrpl/core/LoadMonitor.h
+++ b/include/xrpl/core/LoadMonitor.h
@@ -5,6 +5,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/core/PeerReservationTable.h b/include/xrpl/core/PeerReservationTable.h
index a9ab894124..e6f6dd622e 100644
--- a/include/xrpl/core/PeerReservationTable.h
+++ b/include/xrpl/core/PeerReservationTable.h
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
diff --git a/include/xrpl/core/PerfLog.h b/include/xrpl/core/PerfLog.h
index ca0d9333a4..f09665e291 100644
--- a/include/xrpl/core/PerfLog.h
+++ b/include/xrpl/core/PerfLog.h
@@ -1,6 +1,7 @@
 #pragma once
 
-#include 
+#include 
+#include 
 #include 
 
 #include 
diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h
index 1d0c9e38f4..50bf2d7c10 100644
--- a/include/xrpl/core/ServiceRegistry.h
+++ b/include/xrpl/core/ServiceRegistry.h
@@ -1,11 +1,17 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 // Forward declarations
diff --git a/include/xrpl/core/StartUpType.h b/include/xrpl/core/StartUpType.h
index 46359ad7b6..6d05149618 100644
--- a/include/xrpl/core/StartUpType.h
+++ b/include/xrpl/core/StartUpType.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/core/detail/semaphore.h b/include/xrpl/core/detail/semaphore.h
index 7bc83f86f5..e40463e322 100644
--- a/include/xrpl/core/detail/semaphore.h
+++ b/include/xrpl/core/detail/semaphore.h
@@ -29,6 +29,7 @@
 #pragma once
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/crypto/RFC1751.h b/include/xrpl/crypto/RFC1751.h
index 19b636b9dc..278f3c207b 100644
--- a/include/xrpl/crypto/RFC1751.h
+++ b/include/xrpl/crypto/RFC1751.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/crypto/csprng.h b/include/xrpl/crypto/csprng.h
index e386d9d11e..cdc6a723c8 100644
--- a/include/xrpl/crypto/csprng.h
+++ b/include/xrpl/crypto/csprng.h
@@ -1,5 +1,8 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/json/JsonPropertyStream.h b/include/xrpl/json/JsonPropertyStream.h
index 47317b9ddb..405a61cd34 100644
--- a/include/xrpl/json/JsonPropertyStream.h
+++ b/include/xrpl/json/JsonPropertyStream.h
@@ -3,6 +3,9 @@
 #include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 /** A PropertyStream::Sink which produces a json::Value of type ValueType::Object. */
diff --git a/include/xrpl/json/Writer.h b/include/xrpl/json/Writer.h
index 87e3e99c7e..024876a43c 100644
--- a/include/xrpl/json/Writer.h
+++ b/include/xrpl/json/Writer.h
@@ -1,11 +1,13 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
 
 namespace json {
 
diff --git a/include/xrpl/json/detail/json_assert.h b/include/xrpl/json/detail/json_assert.h
index 8e33f45b65..f501e42aa4 100644
--- a/include/xrpl/json/detail/json_assert.h
+++ b/include/xrpl/json/detail/json_assert.h
@@ -1,8 +1,5 @@
 #pragma once
 
-#include 
-#include 
-
 #define JSON_ASSERT_MESSAGE(condition, message) \
     if (!(condition))                           \
         xrpl::Throw(message);
diff --git a/include/xrpl/json/json_reader.h b/include/xrpl/json/json_reader.h
index 9251183281..c1535bfb55 100644
--- a/include/xrpl/json/json_reader.h
+++ b/include/xrpl/json/json_reader.h
@@ -5,7 +5,10 @@
 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 
 namespace json {
 
diff --git a/include/xrpl/json/json_writer.h b/include/xrpl/json/json_writer.h
index afc99fe8c9..4bc15b71da 100644
--- a/include/xrpl/json/json_writer.h
+++ b/include/xrpl/json/json_writer.h
@@ -3,7 +3,10 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 namespace json {
diff --git a/include/xrpl/ledger/AcceptedLedgerTx.h b/include/xrpl/ledger/AcceptedLedgerTx.h
index 0a1592f6e1..f59b8a074d 100644
--- a/include/xrpl/ledger/AcceptedLedgerTx.h
+++ b/include/xrpl/ledger/AcceptedLedgerTx.h
@@ -1,13 +1,23 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/ledger/AmendmentTable.h b/include/xrpl/ledger/AmendmentTable.h
index 8ed3cb81ff..6598be5a5c 100644
--- a/include/xrpl/ledger/AmendmentTable.h
+++ b/include/xrpl/ledger/AmendmentTable.h
@@ -1,14 +1,37 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h
index 362eae0f79..3bf5d479d1 100644
--- a/include/xrpl/ledger/ApplyView.h
+++ b/include/xrpl/ledger/ApplyView.h
@@ -1,9 +1,22 @@
 #pragma once
 
+#include 
 #include 
 #include 
-#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h
index 1245568630..9a3734a8ca 100644
--- a/include/xrpl/ledger/ApplyViewImpl.h
+++ b/include/xrpl/ledger/ApplyViewImpl.h
@@ -1,9 +1,20 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h
index 36798934da..dc4361136d 100644
--- a/include/xrpl/ledger/BookDirs.h
+++ b/include/xrpl/ledger/BookDirs.h
@@ -1,6 +1,13 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h
index 462db48ee3..f83c3e1297 100644
--- a/include/xrpl/ledger/CachedView.h
+++ b/include/xrpl/ledger/CachedView.h
@@ -1,11 +1,20 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h
index 4dffadd52f..f8349dfab6 100644
--- a/include/xrpl/ledger/CanonicalTXSet.h
+++ b/include/xrpl/ledger/CanonicalTXSet.h
@@ -1,10 +1,16 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Holds transactions which were deferred to the next pass of consensus.
diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h
index d305e21938..05df887d8b 100644
--- a/include/xrpl/ledger/Dir.h
+++ b/include/xrpl/ledger/Dir.h
@@ -1,7 +1,15 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/Ledger.h b/include/xrpl/ledger/Ledger.h
index 5f7d79c61d..3453389a5e 100644
--- a/include/xrpl/ledger/Ledger.h
+++ b/include/xrpl/ledger/Ledger.h
@@ -1,16 +1,32 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/LedgerTiming.h b/include/xrpl/ledger/LedgerTiming.h
index 508403d760..a97e229046 100644
--- a/include/xrpl/ledger/LedgerTiming.h
+++ b/include/xrpl/ledger/LedgerTiming.h
@@ -1,9 +1,10 @@
 #pragma once
 
-#include 
-#include 
+#include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h
index d145473516..de81787906 100644
--- a/include/xrpl/ledger/OpenView.h
+++ b/include/xrpl/ledger/OpenView.h
@@ -1,15 +1,26 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 #include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h
index 1cd89d9388..0117a962ff 100644
--- a/include/xrpl/ledger/PaymentSandbox.h
+++ b/include/xrpl/ledger/PaymentSandbox.h
@@ -1,11 +1,19 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/ledger/RawView.h b/include/xrpl/ledger/RawView.h
index cf61c3e814..ac2674226f 100644
--- a/include/xrpl/ledger/RawView.h
+++ b/include/xrpl/ledger/RawView.h
@@ -3,6 +3,9 @@
 #include 
 #include 
 #include 
+#include 
+
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h
index f4ee7e6fd2..8bbd3e06cb 100644
--- a/include/xrpl/ledger/ReadView.h
+++ b/include/xrpl/ledger/ReadView.h
@@ -1,21 +1,28 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
-#include 
-#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/Sandbox.h b/include/xrpl/ledger/Sandbox.h
index dc80df5ba2..ca8838631f 100644
--- a/include/xrpl/ledger/Sandbox.h
+++ b/include/xrpl/ledger/Sandbox.h
@@ -1,6 +1,8 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h
index 255413e459..c89764df1d 100644
--- a/include/xrpl/ledger/View.h
+++ b/include/xrpl/ledger/View.h
@@ -1,13 +1,22 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
diff --git a/include/xrpl/ledger/detail/ApplyStateTable.h b/include/xrpl/ledger/detail/ApplyStateTable.h
index f40e3d0d1c..752c87d588 100644
--- a/include/xrpl/ledger/detail/ApplyStateTable.h
+++ b/include/xrpl/ledger/detail/ApplyStateTable.h
@@ -1,13 +1,26 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::detail {
 
 // Helper class that buffers modifications
diff --git a/include/xrpl/ledger/detail/ApplyViewBase.h b/include/xrpl/ledger/detail/ApplyViewBase.h
index d6493c46a8..b5b01de277 100644
--- a/include/xrpl/ledger/detail/ApplyViewBase.h
+++ b/include/xrpl/ledger/detail/ApplyViewBase.h
@@ -1,10 +1,20 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl::detail {
 
 class ApplyViewBase : public ApplyView, public RawView
diff --git a/include/xrpl/ledger/detail/RawStateTable.h b/include/xrpl/ledger/detail/RawStateTable.h
index d2567e34f1..cfa528d355 100644
--- a/include/xrpl/ledger/detail/RawStateTable.h
+++ b/include/xrpl/ledger/detail/RawStateTable.h
@@ -1,12 +1,20 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 #include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl::detail {
diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h
index c548ccb101..82d8b59c6a 100644
--- a/include/xrpl/ledger/detail/ReadViewFwdRange.h
+++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h
@@ -1,8 +1,10 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h
index de8bb9d3f7..c6a2053010 100644
--- a/include/xrpl/ledger/helpers/AMMHelpers.h
+++ b/include/xrpl/ledger/helpers/AMMHelpers.h
@@ -2,22 +2,36 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
-#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h
index cf6082d533..d0cfc175a3 100644
--- a/include/xrpl/ledger/helpers/AccountRootHelpers.h
+++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h
@@ -1,13 +1,18 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h
index d6b797ce34..8e78a00923 100644
--- a/include/xrpl/ledger/helpers/CredentialHelpers.h
+++ b/include/xrpl/ledger/helpers/CredentialHelpers.h
@@ -1,15 +1,22 @@
 #pragma once
 
-#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 namespace credentials {
 
diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h
index a517eefdaa..3c277cb4f7 100644
--- a/include/xrpl/ledger/helpers/DelegateHelpers.h
+++ b/include/xrpl/ledger/helpers/DelegateHelpers.h
@@ -4,6 +4,9 @@
 #include 
 #include 
 #include 
+#include 
+
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h b/include/xrpl/ledger/helpers/DirectoryHelpers.h
index a0be52df99..76a5f3bdad 100644
--- a/include/xrpl/ledger/helpers/DirectoryHelpers.h
+++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h
@@ -1,12 +1,16 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
-#include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h
index dc7c479c42..001dc9cb25 100644
--- a/include/xrpl/ledger/helpers/EscrowHelpers.h
+++ b/include/xrpl/ledger/helpers/EscrowHelpers.h
@@ -1,15 +1,28 @@
 #pragma once
 
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h
index 8de945233b..c21e5bf0ce 100644
--- a/include/xrpl/ledger/helpers/LendingHelpers.h
+++ b/include/xrpl/ledger/helpers/LendingHelpers.h
@@ -1,11 +1,28 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h
index b7f87337cf..a725871231 100644
--- a/include/xrpl/ledger/helpers/MPTokenHelpers.h
+++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h
@@ -4,11 +4,17 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h
index 362cfe5a8c..1c4d395fbe 100644
--- a/include/xrpl/ledger/helpers/NFTokenHelpers.h
+++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h
@@ -1,13 +1,25 @@
 #pragma once
 
-#include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl::nft {
diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h
index 3c08ee9f32..6e8cd17f7f 100644
--- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h
+++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h
@@ -1,12 +1,13 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
-#include 
 
 #include 
-#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h
index 695a4950f0..12681257aa 100644
--- a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h
+++ b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h
@@ -1,6 +1,10 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::permissioned_dex {
 
diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h
index 3aaaa541fd..ab09b931cc 100644
--- a/include/xrpl/ledger/helpers/RippleStateHelpers.h
+++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h
@@ -1,14 +1,21 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
 
 //------------------------------------------------------------------------------
 //
diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h
index c8a67f776a..32f785a0d6 100644
--- a/include/xrpl/ledger/helpers/TokenHelpers.h
+++ b/include/xrpl/ledger/helpers/TokenHelpers.h
@@ -1,15 +1,22 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/net/AutoSocket.h b/include/xrpl/net/AutoSocket.h
index 16ed0d6ca9..72eaed7439 100644
--- a/include/xrpl/net/AutoSocket.h
+++ b/include/xrpl/net/AutoSocket.h
@@ -2,12 +2,20 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 // Socket wrapper that supports both SSL and non-SSL connections.
 // Generally, handle it as you would an SSL connection.
 // To force a non-SSL connection, just don't call async_handshake.
diff --git a/include/xrpl/net/HTTPClient.h b/include/xrpl/net/HTTPClient.h
index 456f769922..7ed9b35b9b 100644
--- a/include/xrpl/net/HTTPClient.h
+++ b/include/xrpl/net/HTTPClient.h
@@ -7,6 +7,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h
index ca1983f141..4192455ec9 100644
--- a/include/xrpl/net/HTTPClientSSLContext.h
+++ b/include/xrpl/net/HTTPClientSSLContext.h
@@ -10,6 +10,14 @@
 #include 
 #include 
 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class HTTPClientSSLContext
diff --git a/include/xrpl/net/RegisterSSLCerts.h b/include/xrpl/net/RegisterSSLCerts.h
index e313b1cb06..5cc9934638 100644
--- a/include/xrpl/net/RegisterSSLCerts.h
+++ b/include/xrpl/net/RegisterSSLCerts.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 
 #include 
 
diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h
index 0061890237..29c4a8b526 100644
--- a/include/xrpl/nodestore/Backend.h
+++ b/include/xrpl/nodestore/Backend.h
@@ -1,8 +1,17 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/Database.h b/include/xrpl/nodestore/Database.h
index 68c5dcefb6..49002ee301 100644
--- a/include/xrpl/nodestore/Database.h
+++ b/include/xrpl/nodestore/Database.h
@@ -1,13 +1,25 @@
 #pragma once
 
-#include 
-#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
-#include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 class Section;
diff --git a/include/xrpl/nodestore/DatabaseRotating.h b/include/xrpl/nodestore/DatabaseRotating.h
index a7deed294a..69eb31261d 100644
--- a/include/xrpl/nodestore/DatabaseRotating.h
+++ b/include/xrpl/nodestore/DatabaseRotating.h
@@ -1,6 +1,13 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/DummyScheduler.h b/include/xrpl/nodestore/DummyScheduler.h
index 472684ff13..f626115786 100644
--- a/include/xrpl/nodestore/DummyScheduler.h
+++ b/include/xrpl/nodestore/DummyScheduler.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/Factory.h b/include/xrpl/nodestore/Factory.h
index 3e6ba76a08..e79ae3e05d 100644
--- a/include/xrpl/nodestore/Factory.h
+++ b/include/xrpl/nodestore/Factory.h
@@ -4,7 +4,11 @@
 #include 
 #include 
 
-#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 class Section;
diff --git a/include/xrpl/nodestore/Manager.h b/include/xrpl/nodestore/Manager.h
index 1c4e5b63cf..f813412846 100644
--- a/include/xrpl/nodestore/Manager.h
+++ b/include/xrpl/nodestore/Manager.h
@@ -1,7 +1,14 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/NodeObject.h b/include/xrpl/nodestore/NodeObject.h
index 04ba391b2b..3f3b75d5f8 100644
--- a/include/xrpl/nodestore/NodeObject.h
+++ b/include/xrpl/nodestore/NodeObject.h
@@ -4,6 +4,10 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 // VFALCO NOTE Intentionally not in the NodeStore namespace
 
 namespace xrpl {
diff --git a/include/xrpl/nodestore/Types.h b/include/xrpl/nodestore/Types.h
index 21c01e9111..eaee82c99e 100644
--- a/include/xrpl/nodestore/Types.h
+++ b/include/xrpl/nodestore/Types.h
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 
 namespace xrpl::NodeStore {
diff --git a/include/xrpl/nodestore/detail/BatchWriter.h b/include/xrpl/nodestore/detail/BatchWriter.h
index b0383838dc..7fa23bcb3e 100644
--- a/include/xrpl/nodestore/detail/BatchWriter.h
+++ b/include/xrpl/nodestore/detail/BatchWriter.h
@@ -1,10 +1,12 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl::NodeStore {
diff --git a/include/xrpl/nodestore/detail/DatabaseNodeImp.h b/include/xrpl/nodestore/detail/DatabaseNodeImp.h
index 38b8763f31..6f2fca682f 100644
--- a/include/xrpl/nodestore/detail/DatabaseNodeImp.h
+++ b/include/xrpl/nodestore/detail/DatabaseNodeImp.h
@@ -1,10 +1,26 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h
index 1ba9435a5f..6343275c76 100644
--- a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h
+++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h
@@ -1,8 +1,19 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h b/include/xrpl/nodestore/detail/DecodedBlob.h
index 90a7b6c9cb..b2d5fc9c26 100644
--- a/include/xrpl/nodestore/detail/DecodedBlob.h
+++ b/include/xrpl/nodestore/detail/DecodedBlob.h
@@ -2,6 +2,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl::NodeStore {
 
 /** Parsed key/value blob into NodeObject components.
diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h b/include/xrpl/nodestore/detail/EncodedBlob.h
index 343e1720a0..3982ab1b95 100644
--- a/include/xrpl/nodestore/detail/EncodedBlob.h
+++ b/include/xrpl/nodestore/detail/EncodedBlob.h
@@ -7,7 +7,10 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/detail/ManagerImp.h b/include/xrpl/nodestore/detail/ManagerImp.h
index 98aec6459b..fc84b0aa57 100644
--- a/include/xrpl/nodestore/detail/ManagerImp.h
+++ b/include/xrpl/nodestore/detail/ManagerImp.h
@@ -1,6 +1,17 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h
index 49238fa34a..56e1fbcf73 100644
--- a/include/xrpl/nodestore/detail/codec.h
+++ b/include/xrpl/nodestore/detail/codec.h
@@ -1,6 +1,10 @@
 #pragma once
 
 // Disable lz4 deprecation warning due to incompatibility with clang attributes
+#include 
+#include 
+#include 
+#include 
 #define LZ4_DISABLE_DEPRECATE_WARNINGS
 
 #include 
diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h
index 0c49274d70..21a13bd6de 100644
--- a/include/xrpl/nodestore/detail/varint.h
+++ b/include/xrpl/nodestore/detail/varint.h
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/proto/org/xrpl/rpc/v1/README.md b/include/xrpl/proto/org/xrpl/rpc/v1/README.md
index e8566ec179..d0ff14cd13 100644
--- a/include/xrpl/proto/org/xrpl/rpc/v1/README.md
+++ b/include/xrpl/proto/org/xrpl/rpc/v1/README.md
@@ -70,9 +70,9 @@ into helper functions (see Tx.cpp or AccountTx.cpp for an example).
 #### Testing
 
 When modifying an existing gRPC method, be sure to test that modification in the
-corresponding, existing unit test. When creating a new gRPC method, implement a class that
-derives from GRPCTestClientBase, and use the newly created class to call the new
-method. See the class `GrpcTxClient` in the file Tx_test.cpp for an example.
+corresponding, existing unit test. When creating a new gRPC method, create a
+client stub with `XRPLedgerAPIService::NewStub` and `grpc::CreateChannel`, and
+use it to call the new method. See `GRPCServerTLS_test.cpp` for an example.
 The gRPC tests are paired with their JSON counterpart, and the tests should
 mirror the JSON test as much as possible.
 
diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h
index a83c8bfa84..c4fccd029a 100644
--- a/include/xrpl/protocol/AMMCore.h
+++ b/include/xrpl/protocol/AMMCore.h
@@ -3,9 +3,14 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 constexpr std::uint16_t kTradingFeeThreshold = 1000;  // 1%
diff --git a/include/xrpl/protocol/AccountID.h b/include/xrpl/protocol/AccountID.h
index 4938812ffa..a7d49246ca 100644
--- a/include/xrpl/protocol/AccountID.h
+++ b/include/xrpl/protocol/AccountID.h
@@ -3,13 +3,17 @@
 #include 
 // VFALCO Uncomment when the header issues are resolved
 // #include 
-#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h
index a5f7ec310f..66ada68d6f 100644
--- a/include/xrpl/protocol/AmountConversions.h
+++ b/include/xrpl/protocol/AmountConversions.h
@@ -1,10 +1,20 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h
index 10b7571641..c3292e6074 100644
--- a/include/xrpl/protocol/ApiVersion.h
+++ b/include/xrpl/protocol/ApiVersion.h
@@ -5,6 +5,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h
index ec9d8db02f..2bf24b19fe 100644
--- a/include/xrpl/protocol/Asset.h
+++ b/include/xrpl/protocol/Asset.h
@@ -2,10 +2,19 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
-#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h
index 4e021442d6..1e4f811fa9 100644
--- a/include/xrpl/protocol/Batch.h
+++ b/include/xrpl/protocol/Batch.h
@@ -1,10 +1,13 @@
 #pragma once
 
+#include 
 #include 
 #include 
-#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 inline void
diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h
index 01dc40075b..476bdba35a 100644
--- a/include/xrpl/protocol/Book.h
+++ b/include/xrpl/protocol/Book.h
@@ -2,10 +2,21 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Specifies an order book.
diff --git a/include/xrpl/protocol/BuildInfo.h b/include/xrpl/protocol/BuildInfo.h
index 47a27339a8..a60c37e714 100644
--- a/include/xrpl/protocol/BuildInfo.h
+++ b/include/xrpl/protocol/BuildInfo.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 /** Versioning information for this build. */
 // VFALCO The namespace is deprecated
diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h
index 5b1bcbf606..325117eed4 100644
--- a/include/xrpl/protocol/ConfidentialTransfer.h
+++ b/include/xrpl/protocol/ConfidentialTransfer.h
@@ -1,22 +1,21 @@
 #pragma once
 
+#include 
 #include 
 #include 
-#include 
-#include 
-#include 
-#include 
-#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
 #include 
-#include 
 #include 
-#include 
+#include 
 #include 
 
-#include 
+#include 
 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h
index f5e67fd572..38b8bc6d76 100644
--- a/include/xrpl/protocol/ErrorCodes.h
+++ b/include/xrpl/protocol/ErrorCodes.h
@@ -1,7 +1,8 @@
 #pragma once
 
 #include 
-#include 
+
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h
index 5de8ca64a9..927fde542a 100644
--- a/include/xrpl/protocol/Feature.h
+++ b/include/xrpl/protocol/Feature.h
@@ -1,10 +1,12 @@
 #pragma once
 
 #include 
+#include 
 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h
index 14bcc068bf..4e79204f4c 100644
--- a/include/xrpl/protocol/Fees.h
+++ b/include/xrpl/protocol/Fees.h
@@ -2,6 +2,9 @@
 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 // Deprecated constant for backwards compatibility with pre-XRPFees amendment.
diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h
index b057f1c245..186ce054f1 100644
--- a/include/xrpl/protocol/IOUAmount.h
+++ b/include/xrpl/protocol/IOUAmount.h
@@ -6,6 +6,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h
index 75a2335f6f..053a66787f 100644
--- a/include/xrpl/protocol/Indexes.h
+++ b/include/xrpl/protocol/Indexes.h
@@ -1,18 +1,25 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
-#include 
 #include 
-#include 
 #include 
 #include 
 
+#include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/InnerObjectFormats.h b/include/xrpl/protocol/InnerObjectFormats.h
index 9d07a21d1c..c8312c3701 100644
--- a/include/xrpl/protocol/InnerObjectFormats.h
+++ b/include/xrpl/protocol/InnerObjectFormats.h
@@ -1,6 +1,8 @@
 #pragma once
 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h
index c8022698d3..3d556e83eb 100644
--- a/include/xrpl/protocol/Issue.h
+++ b/include/xrpl/protocol/Issue.h
@@ -1,9 +1,13 @@
 #pragma once
 
-#include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** A currency issued by an account.
diff --git a/include/xrpl/protocol/KnownFormats.h b/include/xrpl/protocol/KnownFormats.h
index 6e21d4bc3a..c31e28c37d 100644
--- a/include/xrpl/protocol/KnownFormats.h
+++ b/include/xrpl/protocol/KnownFormats.h
@@ -7,7 +7,11 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h
index 70afd12f34..5b8a8cc2c5 100644
--- a/include/xrpl/protocol/LedgerFormats.h
+++ b/include/xrpl/protocol/LedgerFormats.h
@@ -3,9 +3,12 @@
 // NOLINTBEGIN(readability-identifier-naming)
 
 #include 
+#include 
 
+#include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/LedgerHeader.h b/include/xrpl/protocol/LedgerHeader.h
index f05b11d1eb..df8f314c5f 100644
--- a/include/xrpl/protocol/LedgerHeader.h
+++ b/include/xrpl/protocol/LedgerHeader.h
@@ -3,10 +3,13 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /** Information about the notional ledger backing the view. */
diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h
index 6ea36fc294..329d83610e 100644
--- a/include/xrpl/protocol/MPTAmount.h
+++ b/include/xrpl/protocol/MPTAmount.h
@@ -2,13 +2,15 @@
 
 #include 
 #include 
-#include 
 #include 
 
 #include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h
index f55029f50d..0c495aa57f 100644
--- a/include/xrpl/protocol/MPTIssue.h
+++ b/include/xrpl/protocol/MPTIssue.h
@@ -1,8 +1,18 @@
 #pragma once
 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /* Adapt MPTID to provide the same interface as Issue. Enables using static
diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h
index b51dc52b47..ebf6fb68a4 100644
--- a/include/xrpl/protocol/PathAsset.h
+++ b/include/xrpl/protocol/PathAsset.h
@@ -1,7 +1,14 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h
index c6f464082d..703a0939c9 100644
--- a/include/xrpl/protocol/Permissions.h
+++ b/include/xrpl/protocol/Permissions.h
@@ -1,9 +1,12 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h
index ba6347dfa7..f802cfe058 100644
--- a/include/xrpl/protocol/Protocol.h
+++ b/include/xrpl/protocol/Protocol.h
@@ -1,7 +1,9 @@
 #pragma once
 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h
index 20693160d3..13db17fc6e 100644
--- a/include/xrpl/protocol/PublicKey.h
+++ b/include/xrpl/protocol/PublicKey.h
@@ -1,8 +1,15 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -11,8 +18,11 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h
index e261025cb8..6bb2033dc0 100644
--- a/include/xrpl/protocol/Quality.h
+++ b/include/xrpl/protocol/Quality.h
@@ -1,11 +1,13 @@
 #pragma once
 
+#include 
+#include 
 #include 
-#include 
 #include 
-#include 
 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h
index 96d30735b8..7830519deb 100644
--- a/include/xrpl/protocol/QualityFunction.h
+++ b/include/xrpl/protocol/QualityFunction.h
@@ -1,9 +1,15 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Average quality of a path as a function of `out`: q(out) = m * out + b,
diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h
index 504b17ed80..b8b04c8fb9 100644
--- a/include/xrpl/protocol/Rate.h
+++ b/include/xrpl/protocol/Rate.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 #include 
 
 #include 
diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h
index 47b20756db..da2031650f 100644
--- a/include/xrpl/protocol/Rules.h
+++ b/include/xrpl/protocol/Rules.h
@@ -4,7 +4,10 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h
index 34fb66ce00..d97bcb0a1d 100644
--- a/include/xrpl/protocol/SField.h
+++ b/include/xrpl/protocol/SField.h
@@ -2,10 +2,11 @@
 
 #include 
 #include 
-#include 
 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h
index 72e0573d29..682a7c655e 100644
--- a/include/xrpl/protocol/SOTemplate.h
+++ b/include/xrpl/protocol/SOTemplate.h
@@ -3,9 +3,11 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/STAccount.h b/include/xrpl/protocol/STAccount.h
index 65f404d58d..17d3affc57 100644
--- a/include/xrpl/protocol/STAccount.h
+++ b/include/xrpl/protocol/STAccount.h
@@ -1,9 +1,13 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h
index 1a5b442d8b..5e53a85129 100644
--- a/include/xrpl/protocol/STAmount.h
+++ b/include/xrpl/protocol/STAmount.h
@@ -1,14 +1,20 @@
 #pragma once
 
 #include 
-#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -16,6 +22,14 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 // Internal form:
diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h
index 61753c52dc..ed39e65026 100644
--- a/include/xrpl/protocol/STArray.h
+++ b/include/xrpl/protocol/STArray.h
@@ -1,7 +1,18 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h
index 6633253d3b..341c80edd7 100644
--- a/include/xrpl/protocol/STBase.h
+++ b/include/xrpl/protocol/STBase.h
@@ -1,9 +1,12 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h
index 0267eac22d..4f25eccca6 100644
--- a/include/xrpl/protocol/STBitString.h
+++ b/include/xrpl/protocol/STBitString.h
@@ -1,8 +1,15 @@
 #pragma once
 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STBlob.h b/include/xrpl/protocol/STBlob.h
index 0667c54e30..ab6175f5e3 100644
--- a/include/xrpl/protocol/STBlob.h
+++ b/include/xrpl/protocol/STBlob.h
@@ -3,10 +3,14 @@
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
+#include 
 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STCurrency.h b/include/xrpl/protocol/STCurrency.h
index 55d1ab1e74..18642b20cf 100644
--- a/include/xrpl/protocol/STCurrency.h
+++ b/include/xrpl/protocol/STCurrency.h
@@ -1,11 +1,15 @@
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class STCurrency final : public STBase
diff --git a/include/xrpl/protocol/STExchange.h b/include/xrpl/protocol/STExchange.h
index c733df37cf..a9c1f57bd8 100644
--- a/include/xrpl/protocol/STExchange.h
+++ b/include/xrpl/protocol/STExchange.h
@@ -1,14 +1,15 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h
index 52e0f7a365..1c7ac98f3e 100644
--- a/include/xrpl/protocol/STInteger.h
+++ b/include/xrpl/protocol/STInteger.h
@@ -1,7 +1,15 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STIssue.h b/include/xrpl/protocol/STIssue.h
index f5e1f61168..8ff579553f 100644
--- a/include/xrpl/protocol/STIssue.h
+++ b/include/xrpl/protocol/STIssue.h
@@ -1,11 +1,20 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class STIssue final : public STBase, CountedObject
diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h
index aa87411ae6..a5f449f99c 100644
--- a/include/xrpl/protocol/STLedgerEntry.h
+++ b/include/xrpl/protocol/STLedgerEntry.h
@@ -1,7 +1,19 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h
index 8594a292f4..7efb63ac5e 100644
--- a/include/xrpl/protocol/STNumber.h
+++ b/include/xrpl/protocol/STNumber.h
@@ -2,10 +2,17 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h
index 044e48ce62..c254a37aaf 100644
--- a/include/xrpl/protocol/STObject.h
+++ b/include/xrpl/protocol/STObject.h
@@ -1,27 +1,39 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
-#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STParsedJSON.h b/include/xrpl/protocol/STParsedJSON.h
index 2557ab055b..1eeecc8b9e 100644
--- a/include/xrpl/protocol/STParsedJSON.h
+++ b/include/xrpl/protocol/STParsedJSON.h
@@ -1,8 +1,11 @@
 #pragma once
 
-#include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h
index 1508dcb727..f6b0fde7da 100644
--- a/include/xrpl/protocol/STPathSet.h
+++ b/include/xrpl/protocol/STPathSet.h
@@ -3,14 +3,17 @@
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STTakesAsset.h b/include/xrpl/protocol/STTakesAsset.h
index bf75ffccf7..70bafd0e91 100644
--- a/include/xrpl/protocol/STTakesAsset.h
+++ b/include/xrpl/protocol/STTakesAsset.h
@@ -3,6 +3,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /** Intermediate class for any STBase-derived class to store an Asset.
diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h
index e78fce27a1..b36207bf61 100644
--- a/include/xrpl/protocol/STTx.h
+++ b/include/xrpl/protocol/STTx.h
@@ -1,18 +1,30 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h
index 91ce88b441..67a6594419 100644
--- a/include/xrpl/protocol/STValidation.h
+++ b/include/xrpl/protocol/STValidation.h
@@ -1,15 +1,30 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
@@ -72,35 +87,35 @@ public:
         F&& f);
 
     // Hash of the validated ledger
-    uint256
+    [[nodiscard]] uint256
     getLedgerHash() const;
 
     // Hash of consensus transaction set used to generate ledger
-    uint256
+    [[nodiscard]] uint256
     getConsensusHash() const;
 
-    NetClock::time_point
+    [[nodiscard]] NetClock::time_point
     getSignTime() const;
 
-    NetClock::time_point
+    [[nodiscard]] NetClock::time_point
     getSeenTime() const noexcept;
 
-    PublicKey const&
+    [[nodiscard]] PublicKey const&
     getSignerPublic() const noexcept;
 
-    NodeID const&
+    [[nodiscard]] NodeID const&
     getNodeID() const noexcept;
 
-    bool
+    [[nodiscard]] bool
     isValid() const noexcept;
 
-    bool
+    [[nodiscard]] bool
     isFull() const noexcept;
 
-    bool
+    [[nodiscard]] bool
     isTrusted() const noexcept;
 
-    uint256
+    [[nodiscard]] uint256
     getSigningHash() const;
 
     void
@@ -112,13 +127,13 @@ public:
     void
     setSeen(NetClock::time_point s);
 
-    Blob
+    [[nodiscard]] Blob
     getSerialized() const;
 
-    Blob
+    [[nodiscard]] Blob
     getSignature() const;
 
-    std::string
+    [[nodiscard]] std::string
     render() const
     {
         std::stringstream ss;
diff --git a/include/xrpl/protocol/STVector256.h b/include/xrpl/protocol/STVector256.h
index 5c454b6be0..46a1abc713 100644
--- a/include/xrpl/protocol/STVector256.h
+++ b/include/xrpl/protocol/STVector256.h
@@ -1,9 +1,15 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
 #include 
-#include 
-#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/STXChainBridge.h b/include/xrpl/protocol/STXChainBridge.h
index 292ffe2767..24d64ef02b 100644
--- a/include/xrpl/protocol/STXChainBridge.h
+++ b/include/xrpl/protocol/STXChainBridge.h
@@ -1,9 +1,19 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/SecretKey.h b/include/xrpl/protocol/SecretKey.h
index 712b095f81..8a0d917ab4 100644
--- a/include/xrpl/protocol/SecretKey.h
+++ b/include/xrpl/protocol/SecretKey.h
@@ -2,14 +2,18 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Seed.h b/include/xrpl/protocol/Seed.h
index 0b93b84516..a669f52079 100644
--- a/include/xrpl/protocol/Seed.h
+++ b/include/xrpl/protocol/Seed.h
@@ -5,7 +5,10 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h
index ffe9afabe8..1d0453d6aa 100644
--- a/include/xrpl/protocol/Serializer.h
+++ b/include/xrpl/protocol/Serializer.h
@@ -6,13 +6,14 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
 
 #include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h
index 0b5b5d7239..18f085352d 100644
--- a/include/xrpl/protocol/Sign.h
+++ b/include/xrpl/protocol/Sign.h
@@ -1,9 +1,13 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h
index 1cc35a0f31..b31dd0cd42 100644
--- a/include/xrpl/protocol/SystemParameters.h
+++ b/include/xrpl/protocol/SystemParameters.h
@@ -1,9 +1,12 @@
 #pragma once
 
+#include 
 #include 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h
index 4ee084e714..b29cb11e15 100644
--- a/include/xrpl/protocol/TER.h
+++ b/include/xrpl/protocol/TER.h
@@ -8,7 +8,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h
index 77ab069feb..36eb6d0889 100644
--- a/include/xrpl/protocol/TxFormats.h
+++ b/include/xrpl/protocol/TxFormats.h
@@ -1,7 +1,9 @@
 #pragma once
 
 #include 
+#include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h
index 6895350e9f..813f1b1615 100644
--- a/include/xrpl/protocol/TxMeta.h
+++ b/include/xrpl/protocol/TxMeta.h
@@ -1,12 +1,21 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 #include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h
index b38c544096..1a3cb96691 100644
--- a/include/xrpl/protocol/UintTypes.h
+++ b/include/xrpl/protocol/UintTypes.h
@@ -1,9 +1,11 @@
 #pragma once
 
-#include 
 #include 
 #include 
-#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 namespace detail {
diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h
index 39f745e84e..8b418704d6 100644
--- a/include/xrpl/protocol/Units.h
+++ b/include/xrpl/protocol/Units.h
@@ -3,14 +3,18 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h
index 1aec5fe549..8f1c7a4ce3 100644
--- a/include/xrpl/protocol/XChainAttestations.h
+++ b/include/xrpl/protocol/XChainAttestations.h
@@ -1,20 +1,20 @@
 #pragma once
 
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
-#include 
 
 #include 
 #include 
 
 #include 
-#include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h
index f09ddc337a..3190980ecb 100644
--- a/include/xrpl/protocol/XRPAmount.h
+++ b/include/xrpl/protocol/XRPAmount.h
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -10,7 +11,11 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h
index 71077d4b33..6853166174 100644
--- a/include/xrpl/protocol/detail/STVar.h
+++ b/include/xrpl/protocol/detail/STVar.h
@@ -5,6 +5,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl::detail {
diff --git a/include/xrpl/protocol/detail/b58_utils.h b/include/xrpl/protocol/detail/b58_utils.h
index e800dbda06..f7aa4e74b4 100644
--- a/include/xrpl/protocol/detail/b58_utils.h
+++ b/include/xrpl/protocol/detail/b58_utils.h
@@ -1,12 +1,14 @@
 #pragma once
 
-#include 
 #include 
 #include 
 
 #include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/detail/token_errors.h b/include/xrpl/protocol/detail/token_errors.h
index 97db9288f9..99a04596bb 100644
--- a/include/xrpl/protocol/detail/token_errors.h
+++ b/include/xrpl/protocol/detail/token_errors.h
@@ -1,6 +1,8 @@
 #pragma once
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 enum class TokenCodecErrc {
diff --git a/include/xrpl/protocol/digest.h b/include/xrpl/protocol/digest.h
index 721ce60767..c1e70cada2 100644
--- a/include/xrpl/protocol/digest.h
+++ b/include/xrpl/protocol/digest.h
@@ -6,6 +6,9 @@
 #include 
 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/protocol/json_get_or_throw.h b/include/xrpl/protocol/json_get_or_throw.h
index 9399f25827..7c7d9afe6c 100644
--- a/include/xrpl/protocol/json_get_or_throw.h
+++ b/include/xrpl/protocol/json_get_or_throw.h
@@ -7,8 +7,12 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 
 namespace json {
 struct JsonMissingKeyError : std::exception
diff --git a/include/xrpl/protocol/messages.h b/include/xrpl/protocol/messages.h
index e6654cc183..dcbe8649c0 100644
--- a/include/xrpl/protocol/messages.h
+++ b/include/xrpl/protocol/messages.h
@@ -10,5 +10,3 @@
 #ifdef TYPE_BOOL
 #undef TYPE_BOOL
 #endif
-
-#include 
diff --git a/include/xrpl/protocol/serialize.h b/include/xrpl/protocol/serialize.h
index 5b84a28b5d..e758b57d49 100644
--- a/include/xrpl/protocol/serialize.h
+++ b/include/xrpl/protocol/serialize.h
@@ -1,9 +1,12 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /** Serialize an object to a blob. */
diff --git a/include/xrpl/protocol/st.h b/include/xrpl/protocol/st.h
deleted file mode 100644
index 61571196f2..0000000000
--- a/include/xrpl/protocol/st.h
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
diff --git a/include/xrpl/protocol/tokens.h b/include/xrpl/protocol/tokens.h
index 67cb25c7fb..bce7e7b5d6 100644
--- a/include/xrpl/protocol/tokens.h
+++ b/include/xrpl/protocol/tokens.h
@@ -1,14 +1,15 @@
 #pragma once
 
-#include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/rdb/DatabaseCon.h b/include/xrpl/rdb/DatabaseCon.h
index 08376f0e71..90aed04337 100644
--- a/include/xrpl/rdb/DatabaseCon.h
+++ b/include/xrpl/rdb/DatabaseCon.h
@@ -1,16 +1,24 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
-#include 
 #include 
 
 #include 
 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
-#include 
 #include 
+#include 
+#include 
 
 namespace soci {
 class session;
diff --git a/include/xrpl/rdb/RelationalDatabase.h b/include/xrpl/rdb/RelationalDatabase.h
index a38d6a997f..91c282ff16 100644
--- a/include/xrpl/rdb/RelationalDatabase.h
+++ b/include/xrpl/rdb/RelationalDatabase.h
@@ -1,18 +1,34 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
-#include 
 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class Transaction;
diff --git a/include/xrpl/rdb/SociDB.h b/include/xrpl/rdb/SociDB.h
index e87aa2a182..0f427bd18b 100644
--- a/include/xrpl/rdb/SociDB.h
+++ b/include/xrpl/rdb/SociDB.h
@@ -8,6 +8,10 @@
     This module requires the @ref beast_sqlite external module.
 */
 
+#include 
+#include 
+
+#include 
 #if defined(__clang__)
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wdeprecated"
@@ -17,7 +21,6 @@
 #include 
 
 #define SOCI_USE_BOOST
-#include 
 
 #include 
 #include 
diff --git a/include/xrpl/resource/Charge.h b/include/xrpl/resource/Charge.h
index 3fd8b02e83..394d641e6c 100644
--- a/include/xrpl/resource/Charge.h
+++ b/include/xrpl/resource/Charge.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include 
+#include 
 #include 
 
 namespace xrpl::Resource {
diff --git a/include/xrpl/resource/Consumer.h b/include/xrpl/resource/Consumer.h
index 14935bb3f1..18207832ac 100644
--- a/include/xrpl/resource/Consumer.h
+++ b/include/xrpl/resource/Consumer.h
@@ -1,10 +1,13 @@
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl::Resource {
 
 struct Entry;
diff --git a/include/xrpl/resource/ResourceManager.h b/include/xrpl/resource/ResourceManager.h
index 42b2a47126..13e0d09343 100644
--- a/include/xrpl/resource/ResourceManager.h
+++ b/include/xrpl/resource/ResourceManager.h
@@ -10,6 +10,10 @@
 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl::Resource {
 
 /** Tracks load and resource consumption. */
diff --git a/include/xrpl/resource/Types.h b/include/xrpl/resource/Types.h
deleted file mode 100644
index fb71cffe37..0000000000
--- a/include/xrpl/resource/Types.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma once
-
-namespace xrpl {
-namespace Resource {
-
-struct Key;
-struct Entry;
-
-}  // namespace Resource
-}  // namespace xrpl
diff --git a/include/xrpl/resource/detail/Entry.h b/include/xrpl/resource/detail/Entry.h
index 361c8409c8..6f44ac2c29 100644
--- a/include/xrpl/resource/detail/Entry.h
+++ b/include/xrpl/resource/detail/Entry.h
@@ -3,10 +3,15 @@
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::Resource {
 
 using clock_type = beast::AbstractClock;
diff --git a/include/xrpl/resource/detail/Import.h b/include/xrpl/resource/detail/Import.h
index 6e2a7b9e7c..a3df6fd73b 100644
--- a/include/xrpl/resource/detail/Import.h
+++ b/include/xrpl/resource/detail/Import.h
@@ -3,6 +3,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl::Resource {
 
 /** A set of imported consumer data from a gossip origin. */
diff --git a/include/xrpl/resource/detail/Key.h b/include/xrpl/resource/detail/Key.h
index 7d24b33955..a0f11422a7 100644
--- a/include/xrpl/resource/detail/Key.h
+++ b/include/xrpl/resource/detail/Key.h
@@ -1,9 +1,10 @@
 #pragma once
 
+#include 
 #include 
-#include 
 #include 
 
+#include 
 #include 
 
 namespace xrpl::Resource {
diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h
index b11ac100f5..6b1c5e7397 100644
--- a/include/xrpl/resource/detail/Logic.h
+++ b/include/xrpl/resource/detail/Logic.h
@@ -4,16 +4,25 @@
 #include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl::Resource {
 
diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h
index f316885fd6..d708a456c4 100644
--- a/include/xrpl/server/InfoSub.h
+++ b/include/xrpl/server/InfoSub.h
@@ -1,13 +1,22 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 // Operations that clients may wish to perform against the network
diff --git a/include/xrpl/server/LoadFeeTrack.h b/include/xrpl/server/LoadFeeTrack.h
index aa32e70ac8..a19ca063d5 100644
--- a/include/xrpl/server/LoadFeeTrack.h
+++ b/include/xrpl/server/LoadFeeTrack.h
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h
index eed1c14dae..eb625d220a 100644
--- a/include/xrpl/server/Manifest.h
+++ b/include/xrpl/server/Manifest.h
@@ -1,14 +1,22 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/NetworkOPs.h b/include/xrpl/server/NetworkOPs.h
index 785d808935..ed2dbbb220 100644
--- a/include/xrpl/server/NetworkOPs.h
+++ b/include/xrpl/server/NetworkOPs.h
@@ -1,16 +1,24 @@
 #pragma once
 
-#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
 #include 
-#include 
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
@@ -25,6 +33,7 @@ class Transaction;
 class ValidatorKeys;
 class CanonicalTXSet;
 class RCLCxPeerPos;
+class SHAMap;
 
 // This is the primary interface into the "client" portion of the program.
 // Code that wants to do normal operations on the network such as
diff --git a/include/xrpl/server/Port.h b/include/xrpl/server/Port.h
index fd773c78fc..c48a2546c1 100644
--- a/include/xrpl/server/Port.h
+++ b/include/xrpl/server/Port.h
@@ -1,7 +1,5 @@
 #pragma once
 
-#include 
-
 #include 
 #include 
 #include 
@@ -11,6 +9,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/server/Server.h b/include/xrpl/server/Server.h
index 4d97fe9877..956a414be8 100644
--- a/include/xrpl/server/Server.h
+++ b/include/xrpl/server/Server.h
@@ -1,12 +1,12 @@
 #pragma once
 
 #include 
-#include 
-#include 
 #include 
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /** Create the HTTP server using the specified handler. */
diff --git a/include/xrpl/server/Session.h b/include/xrpl/server/Session.h
index 151e57e7f2..266570862a 100644
--- a/include/xrpl/server/Session.h
+++ b/include/xrpl/server/Session.h
@@ -2,16 +2,16 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
 #include 
 
-#include 
+#include 
 #include 
-#include 
 #include 
-#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/SimpleWriter.h b/include/xrpl/server/SimpleWriter.h
index dfb0b165e8..996403eafb 100644
--- a/include/xrpl/server/SimpleWriter.h
+++ b/include/xrpl/server/SimpleWriter.h
@@ -7,7 +7,10 @@
 #include 
 #include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/State.h b/include/xrpl/server/State.h
index c3cc4f609c..8590f6e18f 100644
--- a/include/xrpl/server/State.h
+++ b/include/xrpl/server/State.h
@@ -2,10 +2,12 @@
 
 #include 
 #include 
-#include 
+#include 
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 struct SavedState
diff --git a/include/xrpl/server/Vacuum.h b/include/xrpl/server/Vacuum.h
index 5f80eced87..7930265e76 100644
--- a/include/xrpl/server/Vacuum.h
+++ b/include/xrpl/server/Vacuum.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/server/WSSession.h b/include/xrpl/server/WSSession.h
index b2fa52c859..0087f9f50f 100644
--- a/include/xrpl/server/WSSession.h
+++ b/include/xrpl/server/WSSession.h
@@ -2,7 +2,6 @@
 
 #include 
 #include 
-#include 
 
 #include 
 #include 
@@ -11,7 +10,9 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/server/Wallet.h b/include/xrpl/server/Wallet.h
index c15014b1ef..eea44db200 100644
--- a/include/xrpl/server/Wallet.h
+++ b/include/xrpl/server/Wallet.h
@@ -1,9 +1,21 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/server/Writer.h b/include/xrpl/server/Writer.h
index 05fea863be..fe9d9519e1 100644
--- a/include/xrpl/server/Writer.h
+++ b/include/xrpl/server/Writer.h
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h
index d26817bc45..f096d3c5a9 100644
--- a/include/xrpl/server/detail/BaseHTTPPeer.h
+++ b/include/xrpl/server/detail/BaseHTTPPeer.h
@@ -2,8 +2,12 @@
 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 
@@ -20,9 +24,12 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -392,7 +399,7 @@ BaseHTTPPeer::write(void const* buf, std::size_t bytes)
     if ([&] {
             std::scoped_lock const lock(mutex_);
             wq_.emplace_back(buf, bytes);
-            return wq_.size() == 1 && wq2_.size() == 0;
+            return wq_.size() == 1 && wq2_.empty();
         }())
     {
         if (!strand_.running_in_this_thread())
diff --git a/include/xrpl/server/detail/BasePeer.h b/include/xrpl/server/detail/BasePeer.h
index edde28981c..5301a2f018 100644
--- a/include/xrpl/server/detail/BasePeer.h
+++ b/include/xrpl/server/detail/BasePeer.h
@@ -1,7 +1,7 @@
 #pragma once
 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -9,6 +9,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h
index 13225dcba1..1b6a9faca3 100644
--- a/include/xrpl/server/detail/BaseWSPeer.h
+++ b/include/xrpl/server/detail/BaseWSPeer.h
@@ -1,10 +1,13 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -16,8 +19,13 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h
index 79d36cae0c..09b0adc5f2 100644
--- a/include/xrpl/server/detail/Door.h
+++ b/include/xrpl/server/detail/Door.h
@@ -2,6 +2,8 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -19,6 +21,9 @@
 #include 
 #include 
 
+#include 
+#include 
+
 #if !BOOST_OS_WINDOWS
 #include 
 
diff --git a/include/xrpl/server/detail/JSONRPCUtil.h b/include/xrpl/server/detail/JSONRPCUtil.h
index b2e87f5332..1c157708c2 100644
--- a/include/xrpl/server/detail/JSONRPCUtil.h
+++ b/include/xrpl/server/detail/JSONRPCUtil.h
@@ -2,7 +2,8 @@
 
 #include 
 #include 
-#include 
+
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/LowestLayer.h b/include/xrpl/server/detail/LowestLayer.h
index d28510d1a5..4daf91cd4c 100644
--- a/include/xrpl/server/detail/LowestLayer.h
+++ b/include/xrpl/server/detail/LowestLayer.h
@@ -1,23 +1,14 @@
 #pragma once
 
-#if BOOST_VERSION >= 107000
 #include 
-#else
-#include 
-#endif
 
 namespace xrpl {
 
-// Before boost 1.70, get_lowest_layer required an explicit template parameter
 template 
 decltype(auto)
 getLowestLayer(T& t) noexcept
 {
-#if BOOST_VERSION >= 107000
     return boost::beast::get_lowest_layer(t);
-#else
-    return t.lowest_layer();
-#endif
 }
 
 }  // namespace xrpl
diff --git a/include/xrpl/server/detail/PlainHTTPPeer.h b/include/xrpl/server/detail/PlainHTTPPeer.h
index 791ab91acf..b89272fe9e 100644
--- a/include/xrpl/server/detail/PlainHTTPPeer.h
+++ b/include/xrpl/server/detail/PlainHTTPPeer.h
@@ -1,12 +1,17 @@
 #pragma once
 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/PlainWSPeer.h b/include/xrpl/server/detail/PlainWSPeer.h
index 593973a3ed..279e256f51 100644
--- a/include/xrpl/server/detail/PlainWSPeer.h
+++ b/include/xrpl/server/detail/PlainWSPeer.h
@@ -1,10 +1,14 @@
 #pragma once
 
+#include 
+#include 
 #include 
 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/SSLHTTPPeer.h b/include/xrpl/server/detail/SSLHTTPPeer.h
index f3b047150f..ea2108b917 100644
--- a/include/xrpl/server/detail/SSLHTTPPeer.h
+++ b/include/xrpl/server/detail/SSLHTTPPeer.h
@@ -1,5 +1,9 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
@@ -9,7 +13,9 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/SSLWSPeer.h b/include/xrpl/server/detail/SSLWSPeer.h
index cb03d5a796..fe522e3c9a 100644
--- a/include/xrpl/server/detail/SSLWSPeer.h
+++ b/include/xrpl/server/detail/SSLWSPeer.h
@@ -1,7 +1,9 @@
 #pragma once
 
-#include 
+#include 
+#include 
 #include 
+#include 
 
 #include 
 #include 
@@ -9,8 +11,11 @@
 #include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/ServerImpl.h b/include/xrpl/server/detail/ServerImpl.h
index 48ffa26cfe..df2bba0284 100644
--- a/include/xrpl/server/detail/ServerImpl.h
+++ b/include/xrpl/server/detail/ServerImpl.h
@@ -1,7 +1,8 @@
 #pragma once
 
-#include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
@@ -11,9 +12,15 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/server/detail/Spawn.h b/include/xrpl/server/detail/Spawn.h
index 2560a2718b..1ba32c2f0b 100644
--- a/include/xrpl/server/detail/Spawn.h
+++ b/include/xrpl/server/detail/Spawn.h
@@ -6,6 +6,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl::util {
diff --git a/include/xrpl/server/detail/io_list.h b/include/xrpl/server/detail/io_list.h
index 4daa23fb7e..0153bd3457 100644
--- a/include/xrpl/server/detail/io_list.h
+++ b/include/xrpl/server/detail/io_list.h
@@ -3,6 +3,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/shamap/Family.h b/include/xrpl/shamap/Family.h
index b9dd85443a..c5bf953bfd 100644
--- a/include/xrpl/shamap/Family.h
+++ b/include/xrpl/shamap/Family.h
@@ -1,11 +1,13 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
 #include 
 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/shamap/FullBelowCache.h b/include/xrpl/shamap/FullBelowCache.h
index e9fd04ac58..dc597278df 100644
--- a/include/xrpl/shamap/FullBelowCache.h
+++ b/include/xrpl/shamap/FullBelowCache.h
@@ -4,9 +4,13 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h
index 3d08318cf6..d49e323b3f 100644
--- a/include/xrpl/shamap/SHAMap.h
+++ b/include/xrpl/shamap/SHAMap.h
@@ -1,11 +1,14 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -14,8 +17,20 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h
index e388d205d1..ee81107f61 100644
--- a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h
+++ b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h
@@ -1,10 +1,17 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/shamap/SHAMapInnerNode.h b/include/xrpl/shamap/SHAMapInnerNode.h
index cafb498218..0fb4e24077 100644
--- a/include/xrpl/shamap/SHAMapInnerNode.h
+++ b/include/xrpl/shamap/SHAMapInnerNode.h
@@ -1,7 +1,11 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
diff --git a/include/xrpl/shamap/SHAMapItem.h b/include/xrpl/shamap/SHAMapItem.h
index 41558197bf..846f0bab4e 100644
--- a/include/xrpl/shamap/SHAMapItem.h
+++ b/include/xrpl/shamap/SHAMapItem.h
@@ -5,10 +5,18 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 // an item stored in a SHAMap
diff --git a/include/xrpl/shamap/SHAMapLeafNode.h b/include/xrpl/shamap/SHAMapLeafNode.h
index af5ed29702..112c243131 100644
--- a/include/xrpl/shamap/SHAMapLeafNode.h
+++ b/include/xrpl/shamap/SHAMapLeafNode.h
@@ -1,9 +1,12 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/shamap/SHAMapMissingNode.h b/include/xrpl/shamap/SHAMapMissingNode.h
index 0bc072463e..3168d0eff1 100644
--- a/include/xrpl/shamap/SHAMapMissingNode.h
+++ b/include/xrpl/shamap/SHAMapMissingNode.h
@@ -1,9 +1,9 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
 
-#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/shamap/SHAMapNodeID.h b/include/xrpl/shamap/SHAMapNodeID.h
index 248c9cb80b..b812c10ca9 100644
--- a/include/xrpl/shamap/SHAMapNodeID.h
+++ b/include/xrpl/shamap/SHAMapNodeID.h
@@ -3,7 +3,9 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/shamap/SHAMapSyncFilter.h b/include/xrpl/shamap/SHAMapSyncFilter.h
index 4104220a3f..b6ce175915 100644
--- a/include/xrpl/shamap/SHAMapSyncFilter.h
+++ b/include/xrpl/shamap/SHAMapSyncFilter.h
@@ -1,7 +1,10 @@
 #pragma once
 
+#include 
+#include 
 #include 
 
+#include 
 #include 
 
 /** Callback for filtering SHAMap during sync. */
diff --git a/include/xrpl/shamap/SHAMapTreeNode.h b/include/xrpl/shamap/SHAMapTreeNode.h
index 5cca2ea41a..1eebbaa17f 100644
--- a/include/xrpl/shamap/SHAMapTreeNode.h
+++ b/include/xrpl/shamap/SHAMapTreeNode.h
@@ -3,8 +3,8 @@
 #include 
 #include 
 #include 
+#include 
 #include 
-#include 
 #include 
 
 #include 
diff --git a/include/xrpl/shamap/SHAMapTxLeafNode.h b/include/xrpl/shamap/SHAMapTxLeafNode.h
index 49f4f90906..86186434f8 100644
--- a/include/xrpl/shamap/SHAMapTxLeafNode.h
+++ b/include/xrpl/shamap/SHAMapTxLeafNode.h
@@ -1,10 +1,17 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h
index 3f4163ac41..9e4573d45b 100644
--- a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h
+++ b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h
@@ -1,10 +1,17 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/shamap/TreeNodeCache.h b/include/xrpl/shamap/TreeNodeCache.h
index 2d5782c7e9..bff03a76e2 100644
--- a/include/xrpl/shamap/TreeNodeCache.h
+++ b/include/xrpl/shamap/TreeNodeCache.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/shamap/detail/TaggedPointer.h b/include/xrpl/shamap/detail/TaggedPointer.h
index 5eb3863de0..79f3464d5b 100644
--- a/include/xrpl/shamap/detail/TaggedPointer.h
+++ b/include/xrpl/shamap/detail/TaggedPointer.h
@@ -1,11 +1,14 @@
 #pragma once
 
-#include 
+#include 
 #include 
 
-#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include   // IWYU pragma: keep
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h
index c98b3f82c5..ca778387cd 100644
--- a/include/xrpl/tx/ApplyContext.h
+++ b/include/xrpl/tx/ApplyContext.h
@@ -1,12 +1,23 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/SignerEntries.h b/include/xrpl/tx/SignerEntries.h
index af0c9b9d28..c8481f6062 100644
--- a/include/xrpl/tx/SignerEntries.h
+++ b/include/xrpl/tx/SignerEntries.h
@@ -1,13 +1,15 @@
 #pragma once
 
+#include 
 #include   // beast::Journal
-#include            // temMALFORMED
-#include      // AccountID
-#include           // NotTEC
+#include 
+#include   // temMALFORMED
 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h
index 2b50cfa6e7..bc5e8c80e7 100644
--- a/include/xrpl/tx/Transactor.h
+++ b/include/xrpl/tx/Transactor.h
@@ -1,14 +1,30 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h
index 49b30fea02..55d365c31b 100644
--- a/include/xrpl/tx/apply.h
+++ b/include/xrpl/tx/apply.h
@@ -1,10 +1,14 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/applySteps.h b/include/xrpl/tx/applySteps.h
index 897cf500d6..3298e49192 100644
--- a/include/xrpl/tx/applySteps.h
+++ b/include/xrpl/tx/applySteps.h
@@ -1,7 +1,19 @@
 #pragma once
 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/invariants/AMMInvariant.h b/include/xrpl/tx/invariants/AMMInvariant.h
index 4b56370774..29d962fb79 100644
--- a/include/xrpl/tx/invariants/AMMInvariant.h
+++ b/include/xrpl/tx/invariants/AMMInvariant.h
@@ -2,9 +2,12 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 #include 
 
diff --git a/include/xrpl/tx/invariants/DirectoryInvariant.h b/include/xrpl/tx/invariants/DirectoryInvariant.h
index 96643ea465..d18ac539cc 100644
--- a/include/xrpl/tx/invariants/DirectoryInvariant.h
+++ b/include/xrpl/tx/invariants/DirectoryInvariant.h
@@ -1,8 +1,10 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h
index a76eb66497..4b3e9beec4 100644
--- a/include/xrpl/tx/invariants/FreezeInvariant.h
+++ b/include/xrpl/tx/invariants/FreezeInvariant.h
@@ -2,10 +2,13 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h
index ba91cba064..d19f34635d 100644
--- a/include/xrpl/tx/invariants/InvariantCheck.h
+++ b/include/xrpl/tx/invariants/InvariantCheck.h
@@ -1,10 +1,11 @@
 #pragma once
 
-#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -17,7 +18,11 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h
index e55419dfc1..b2f1c62a54 100644
--- a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h
+++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include 
 #include 
 
 #include 
diff --git a/include/xrpl/tx/invariants/LoanBrokerInvariant.h b/include/xrpl/tx/invariants/LoanBrokerInvariant.h
index 684bbff423..979f57de35 100644
--- a/include/xrpl/tx/invariants/LoanBrokerInvariant.h
+++ b/include/xrpl/tx/invariants/LoanBrokerInvariant.h
@@ -3,8 +3,10 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h
index 3f408d169a..0648881423 100644
--- a/include/xrpl/tx/invariants/LoanInvariant.h
+++ b/include/xrpl/tx/invariants/LoanInvariant.h
@@ -2,9 +2,12 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h
index aa90f1b8ef..1d39fd13d8 100644
--- a/include/xrpl/tx/invariants/MPTInvariant.h
+++ b/include/xrpl/tx/invariants/MPTInvariant.h
@@ -1,13 +1,21 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/invariants/NFTInvariant.h b/include/xrpl/tx/invariants/NFTInvariant.h
index 698df05247..11bdf5a569 100644
--- a/include/xrpl/tx/invariants/NFTInvariant.h
+++ b/include/xrpl/tx/invariants/NFTInvariant.h
@@ -1,10 +1,11 @@
 #pragma once
 
-#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 #include 
 
diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h
index 2ec22ded88..2763b80a94 100644
--- a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h
+++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h
@@ -1,10 +1,13 @@
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h
index 19edcc0b39..fae7c24d25 100644
--- a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h
+++ b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h
@@ -2,9 +2,12 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h
index 2a9ffc8282..bc8246b234 100644
--- a/include/xrpl/tx/invariants/VaultInvariant.h
+++ b/include/xrpl/tx/invariants/VaultInvariant.h
@@ -3,13 +3,18 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/include/xrpl/tx/paths/AMMLiquidity.h b/include/xrpl/tx/paths/AMMLiquidity.h
index 9abe37f868..bf62155b61 100644
--- a/include/xrpl/tx/paths/AMMLiquidity.h
+++ b/include/xrpl/tx/paths/AMMLiquidity.h
@@ -1,13 +1,17 @@
 #pragma once
 
-#include 
+#include 
+#include 
 #include 
-#include 
-#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 template 
diff --git a/include/xrpl/tx/paths/AMMOffer.h b/include/xrpl/tx/paths/AMMOffer.h
index 21c45c36a3..40a9cf40b3 100644
--- a/include/xrpl/tx/paths/AMMOffer.h
+++ b/include/xrpl/tx/paths/AMMOffer.h
@@ -1,12 +1,18 @@
 #pragma once
 
+#include 
+#include 
 #include 
-#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 template 
diff --git a/include/xrpl/tx/paths/BookTip.h b/include/xrpl/tx/paths/BookTip.h
index c4bdb0415c..bad007ca5b 100644
--- a/include/xrpl/tx/paths/BookTip.h
+++ b/include/xrpl/tx/paths/BookTip.h
@@ -1,8 +1,11 @@
 #pragma once
 
-#include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/paths/Flow.h b/include/xrpl/tx/paths/Flow.h
index c746249866..f73b9a3440 100644
--- a/include/xrpl/tx/paths/Flow.h
+++ b/include/xrpl/tx/paths/Flow.h
@@ -1,9 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 namespace path::detail {
diff --git a/include/xrpl/tx/paths/Offer.h b/include/xrpl/tx/paths/Offer.h
index 2dab5bcebf..164a933ba1 100644
--- a/include/xrpl/tx/paths/Offer.h
+++ b/include/xrpl/tx/paths/Offer.h
@@ -1,16 +1,26 @@
 #pragma once
 
 #include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/paths/OfferStream.h b/include/xrpl/tx/paths/OfferStream.h
index 69409b9ef7..98c58876c9 100644
--- a/include/xrpl/tx/paths/OfferStream.h
+++ b/include/xrpl/tx/paths/OfferStream.h
@@ -1,15 +1,20 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
-#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 template 
diff --git a/include/xrpl/tx/paths/RippleCalc.h b/include/xrpl/tx/paths/RippleCalc.h
index c747787820..62c966d384 100644
--- a/include/xrpl/tx/paths/RippleCalc.h
+++ b/include/xrpl/tx/paths/RippleCalc.h
@@ -1,12 +1,17 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+
 namespace xrpl {
 class Config;
 namespace path {
diff --git a/include/xrpl/tx/paths/detail/EitherAmount.h b/include/xrpl/tx/paths/detail/EitherAmount.h
index ffd90751b8..68ad90d2d4 100644
--- a/include/xrpl/tx/paths/detail/EitherAmount.h
+++ b/include/xrpl/tx/paths/detail/EitherAmount.h
@@ -1,10 +1,15 @@
 #pragma once
 
+#include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 struct EitherAmount
diff --git a/include/xrpl/tx/paths/detail/FlowDebugInfo.h b/include/xrpl/tx/paths/detail/FlowDebugInfo.h
index 1ccfba34ce..48066840f5 100644
--- a/include/xrpl/tx/paths/detail/FlowDebugInfo.h
+++ b/include/xrpl/tx/paths/detail/FlowDebugInfo.h
@@ -1,14 +1,22 @@
 #pragma once
 
-#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 #include 
 
 #include 
-#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::path::detail {
 // Track performance information of a single payment
diff --git a/include/xrpl/tx/paths/detail/StepChecks.h b/include/xrpl/tx/paths/detail/StepChecks.h
index 4955c4f8e6..f62613e95c 100644
--- a/include/xrpl/tx/paths/detail/StepChecks.h
+++ b/include/xrpl/tx/paths/detail/StepChecks.h
@@ -2,9 +2,15 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/paths/detail/Steps.h b/include/xrpl/tx/paths/detail/Steps.h
index 9909c4bdcd..7eb6b938a5 100644
--- a/include/xrpl/tx/paths/detail/Steps.h
+++ b/include/xrpl/tx/paths/detail/Steps.h
@@ -1,16 +1,30 @@
 #pragma once
 
-#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 class PaymentSandbox;
diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h
index 4d988a4c7e..fe657b2100 100644
--- a/include/xrpl/tx/paths/detail/StrandFlow.h
+++ b/include/xrpl/tx/paths/detail/StrandFlow.h
@@ -1,12 +1,21 @@
 #pragma once
 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -17,8 +26,16 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/transactors/account/AccountDelete.h b/include/xrpl/tx/transactors/account/AccountDelete.h
index 16661a4b7c..e69d948bb8 100644
--- a/include/xrpl/tx/transactors/account/AccountDelete.h
+++ b/include/xrpl/tx/transactors/account/AccountDelete.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/account/AccountSet.h b/include/xrpl/tx/transactors/account/AccountSet.h
index 91b38e7968..4326621f3d 100644
--- a/include/xrpl/tx/transactors/account/AccountSet.h
+++ b/include/xrpl/tx/transactors/account/AccountSet.h
@@ -1,8 +1,16 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class AccountSet : public Transactor
diff --git a/include/xrpl/tx/transactors/account/SetRegularKey.h b/include/xrpl/tx/transactors/account/SetRegularKey.h
index 6ff9c5aa52..90e4cd96cd 100644
--- a/include/xrpl/tx/transactors/account/SetRegularKey.h
+++ b/include/xrpl/tx/transactors/account/SetRegularKey.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/account/SignerListSet.h b/include/xrpl/tx/transactors/account/SignerListSet.h
index 46e3191323..da2274a1de 100644
--- a/include/xrpl/tx/transactors/account/SignerListSet.h
+++ b/include/xrpl/tx/transactors/account/SignerListSet.h
@@ -1,11 +1,20 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/bridge/XChainBridge.h b/include/xrpl/tx/transactors/bridge/XChainBridge.h
index 58a546de2f..24f49eb340 100644
--- a/include/xrpl/tx/transactors/bridge/XChainBridge.h
+++ b/include/xrpl/tx/transactors/bridge/XChainBridge.h
@@ -1,8 +1,17 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 constexpr size_t kXbridgeMaxAccountCreateClaims = 128;
diff --git a/include/xrpl/tx/transactors/check/CheckCancel.h b/include/xrpl/tx/transactors/check/CheckCancel.h
index b8e8b6c52d..b54d1e6c84 100644
--- a/include/xrpl/tx/transactors/check/CheckCancel.h
+++ b/include/xrpl/tx/transactors/check/CheckCancel.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/check/CheckCash.h b/include/xrpl/tx/transactors/check/CheckCash.h
index 7d4e615cfd..4656f941f4 100644
--- a/include/xrpl/tx/transactors/check/CheckCash.h
+++ b/include/xrpl/tx/transactors/check/CheckCash.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/check/CheckCreate.h b/include/xrpl/tx/transactors/check/CheckCreate.h
index 178fe4707c..50a1f076e0 100644
--- a/include/xrpl/tx/transactors/check/CheckCreate.h
+++ b/include/xrpl/tx/transactors/check/CheckCreate.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/credentials/CredentialAccept.h b/include/xrpl/tx/transactors/credentials/CredentialAccept.h
index 8630ac3f7f..b9dfa684e7 100644
--- a/include/xrpl/tx/transactors/credentials/CredentialAccept.h
+++ b/include/xrpl/tx/transactors/credentials/CredentialAccept.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class CredentialAccept : public Transactor
diff --git a/include/xrpl/tx/transactors/credentials/CredentialCreate.h b/include/xrpl/tx/transactors/credentials/CredentialCreate.h
index 91b5e829d3..ce9f9305be 100644
--- a/include/xrpl/tx/transactors/credentials/CredentialCreate.h
+++ b/include/xrpl/tx/transactors/credentials/CredentialCreate.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class CredentialCreate : public Transactor
diff --git a/include/xrpl/tx/transactors/credentials/CredentialDelete.h b/include/xrpl/tx/transactors/credentials/CredentialDelete.h
index 70fe5cb3d0..2f151a9fa0 100644
--- a/include/xrpl/tx/transactors/credentials/CredentialDelete.h
+++ b/include/xrpl/tx/transactors/credentials/CredentialDelete.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class CredentialDelete : public Transactor
diff --git a/include/xrpl/tx/transactors/delegate/DelegateSet.h b/include/xrpl/tx/transactors/delegate/DelegateSet.h
index a55524ecff..b7141276de 100644
--- a/include/xrpl/tx/transactors/delegate/DelegateSet.h
+++ b/include/xrpl/tx/transactors/delegate/DelegateSet.h
@@ -1,5 +1,13 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/dex/AMMBid.h b/include/xrpl/tx/transactors/dex/AMMBid.h
index dfa50d06ba..fa257696e8 100644
--- a/include/xrpl/tx/transactors/dex/AMMBid.h
+++ b/include/xrpl/tx/transactors/dex/AMMBid.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/dex/AMMClawback.h b/include/xrpl/tx/transactors/dex/AMMClawback.h
index 6f31480490..54eb3c9f27 100644
--- a/include/xrpl/tx/transactors/dex/AMMClawback.h
+++ b/include/xrpl/tx/transactors/dex/AMMClawback.h
@@ -1,7 +1,20 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 class Sandbox;
 class AMMClawback : public Transactor
diff --git a/include/xrpl/tx/transactors/dex/AMMCreate.h b/include/xrpl/tx/transactors/dex/AMMCreate.h
index 64d2a1e3b1..188af8d4ef 100644
--- a/include/xrpl/tx/transactors/dex/AMMCreate.h
+++ b/include/xrpl/tx/transactors/dex/AMMCreate.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/dex/AMMDelete.h b/include/xrpl/tx/transactors/dex/AMMDelete.h
index d3e8cfeeb4..4a0905fe10 100644
--- a/include/xrpl/tx/transactors/dex/AMMDelete.h
+++ b/include/xrpl/tx/transactors/dex/AMMDelete.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/dex/AMMDeposit.h b/include/xrpl/tx/transactors/dex/AMMDeposit.h
index 453046ddad..b87db19b2d 100644
--- a/include/xrpl/tx/transactors/dex/AMMDeposit.h
+++ b/include/xrpl/tx/transactors/dex/AMMDeposit.h
@@ -1,7 +1,21 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class Sandbox;
diff --git a/include/xrpl/tx/transactors/dex/AMMVote.h b/include/xrpl/tx/transactors/dex/AMMVote.h
index 8defc1369e..10ad284bb3 100644
--- a/include/xrpl/tx/transactors/dex/AMMVote.h
+++ b/include/xrpl/tx/transactors/dex/AMMVote.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h
index 8b6d39349b..8f6700037d 100644
--- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h
+++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h
@@ -1,9 +1,23 @@
 #pragma once
 
-#include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class Sandbox;
diff --git a/include/xrpl/tx/transactors/dex/OfferCancel.h b/include/xrpl/tx/transactors/dex/OfferCancel.h
index 2806b6942f..974478475c 100644
--- a/include/xrpl/tx/transactors/dex/OfferCancel.h
+++ b/include/xrpl/tx/transactors/dex/OfferCancel.h
@@ -1,6 +1,12 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/dex/OfferCreate.h b/include/xrpl/tx/transactors/dex/OfferCreate.h
index 7faf613d3a..a3bf7626f9 100644
--- a/include/xrpl/tx/transactors/dex/OfferCreate.h
+++ b/include/xrpl/tx/transactors/dex/OfferCreate.h
@@ -1,8 +1,28 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class PaymentSandbox;
diff --git a/include/xrpl/tx/transactors/did/DIDDelete.h b/include/xrpl/tx/transactors/did/DIDDelete.h
index 94615f51b6..c119b0348e 100644
--- a/include/xrpl/tx/transactors/did/DIDDelete.h
+++ b/include/xrpl/tx/transactors/did/DIDDelete.h
@@ -1,5 +1,15 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/did/DIDSet.h b/include/xrpl/tx/transactors/did/DIDSet.h
index 8c84ea6c9b..481222243b 100644
--- a/include/xrpl/tx/transactors/did/DIDSet.h
+++ b/include/xrpl/tx/transactors/did/DIDSet.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/escrow/EscrowCancel.h b/include/xrpl/tx/transactors/escrow/EscrowCancel.h
index af09f70202..b52fb2a9fa 100644
--- a/include/xrpl/tx/transactors/escrow/EscrowCancel.h
+++ b/include/xrpl/tx/transactors/escrow/EscrowCancel.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/escrow/EscrowCreate.h b/include/xrpl/tx/transactors/escrow/EscrowCreate.h
index 8800e97b80..b8e22aa24e 100644
--- a/include/xrpl/tx/transactors/escrow/EscrowCreate.h
+++ b/include/xrpl/tx/transactors/escrow/EscrowCreate.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/escrow/EscrowFinish.h b/include/xrpl/tx/transactors/escrow/EscrowFinish.h
index 061fa0527c..07459bf959 100644
--- a/include/xrpl/tx/transactors/escrow/EscrowFinish.h
+++ b/include/xrpl/tx/transactors/escrow/EscrowFinish.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h
index 81ea97ce7e..cd72a01bc3 100644
--- a/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h
+++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h
index 43b932726b..03ed295766 100644
--- a/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h
+++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h
index a757ac51bf..1132211e58 100644
--- a/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h
+++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h
index 0ce5f29387..7a9a529510 100644
--- a/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h
+++ b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerSet.h b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h
index 75175e2dd6..e7137c9311 100644
--- a/include/xrpl/tx/transactors/lending/LoanBrokerSet.h
+++ b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h
@@ -1,7 +1,17 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class LoanBrokerSet : public Transactor
diff --git a/include/xrpl/tx/transactors/lending/LoanDelete.h b/include/xrpl/tx/transactors/lending/LoanDelete.h
index 9e8c3c172a..604388a2bd 100644
--- a/include/xrpl/tx/transactors/lending/LoanDelete.h
+++ b/include/xrpl/tx/transactors/lending/LoanDelete.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/lending/LoanManage.h b/include/xrpl/tx/transactors/lending/LoanManage.h
index d2344c0ec0..c8a5584131 100644
--- a/include/xrpl/tx/transactors/lending/LoanManage.h
+++ b/include/xrpl/tx/transactors/lending/LoanManage.h
@@ -1,7 +1,18 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class LoanManage : public Transactor
diff --git a/include/xrpl/tx/transactors/lending/LoanPay.h b/include/xrpl/tx/transactors/lending/LoanPay.h
index 9be9695c64..599bea6ae6 100644
--- a/include/xrpl/tx/transactors/lending/LoanPay.h
+++ b/include/xrpl/tx/transactors/lending/LoanPay.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class LoanPay : public Transactor
diff --git a/include/xrpl/tx/transactors/lending/LoanSet.h b/include/xrpl/tx/transactors/lending/LoanSet.h
index d277629e44..fab489e3db 100644
--- a/include/xrpl/tx/transactors/lending/LoanSet.h
+++ b/include/xrpl/tx/transactors/lending/LoanSet.h
@@ -1,8 +1,19 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 class LoanSet : public Transactor
diff --git a/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h
index c5cd10fa6a..28037ae3b9 100644
--- a/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h
+++ b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h
@@ -1,5 +1,15 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/nft/NFTokenBurn.h b/include/xrpl/tx/transactors/nft/NFTokenBurn.h
index 849d09cb7e..a9640e3a9e 100644
--- a/include/xrpl/tx/transactors/nft/NFTokenBurn.h
+++ b/include/xrpl/tx/transactors/nft/NFTokenBurn.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h
index a74a1c3e59..35c9067f63 100644
--- a/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h
+++ b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h
index c874381dd0..f11ac5c373 100644
--- a/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h
+++ b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class NFTokenCreateOffer : public Transactor
diff --git a/include/xrpl/tx/transactors/nft/NFTokenMint.h b/include/xrpl/tx/transactors/nft/NFTokenMint.h
index 9267e8e801..9d5874fa60 100644
--- a/include/xrpl/tx/transactors/nft/NFTokenMint.h
+++ b/include/xrpl/tx/transactors/nft/NFTokenMint.h
@@ -1,9 +1,19 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class NFTokenMint : public Transactor
diff --git a/include/xrpl/tx/transactors/nft/NFTokenModify.h b/include/xrpl/tx/transactors/nft/NFTokenModify.h
index 0d18e4a6d4..970914bbfe 100644
--- a/include/xrpl/tx/transactors/nft/NFTokenModify.h
+++ b/include/xrpl/tx/transactors/nft/NFTokenModify.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/oracle/OracleDelete.h b/include/xrpl/tx/transactors/oracle/OracleDelete.h
index c16d5fb2a9..f9e9230527 100644
--- a/include/xrpl/tx/transactors/oracle/OracleDelete.h
+++ b/include/xrpl/tx/transactors/oracle/OracleDelete.h
@@ -1,5 +1,14 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h b/include/xrpl/tx/transactors/oracle/OracleSet.h
index 831c11b8c4..e95970923b 100644
--- a/include/xrpl/tx/transactors/oracle/OracleSet.h
+++ b/include/xrpl/tx/transactors/oracle/OracleSet.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/payment/DepositPreauth.h b/include/xrpl/tx/transactors/payment/DepositPreauth.h
index 742b1ef3f7..b9b42ddac6 100644
--- a/include/xrpl/tx/transactors/payment/DepositPreauth.h
+++ b/include/xrpl/tx/transactors/payment/DepositPreauth.h
@@ -1,5 +1,14 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/payment/Payment.h b/include/xrpl/tx/transactors/payment/Payment.h
index dd792aa1c2..1356a368e9 100644
--- a/include/xrpl/tx/transactors/payment/Payment.h
+++ b/include/xrpl/tx/transactors/payment/Payment.h
@@ -1,7 +1,19 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class Payment : public Transactor
diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h
index e13fea6d6c..85df2a02e7 100644
--- a/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h
+++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class PaymentChannelClaim : public Transactor
diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h
index 56e984cd57..f0c9da6349 100644
--- a/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h
+++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h
index 272076ff20..377fbe9cf2 100644
--- a/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h
+++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h
index 88883fb86f..5a07262a3b 100644
--- a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h
+++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h
index 4afa8cef5a..38de800284 100644
--- a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h
+++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/system/Batch.h b/include/xrpl/tx/transactors/system/Batch.h
index 927a5b27cd..6540005877 100644
--- a/include/xrpl/tx/transactors/system/Batch.h
+++ b/include/xrpl/tx/transactors/system/Batch.h
@@ -1,7 +1,18 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 class Batch : public Transactor
diff --git a/include/xrpl/tx/transactors/system/Change.h b/include/xrpl/tx/transactors/system/Change.h
index 339723ae8e..b25208f075 100644
--- a/include/xrpl/tx/transactors/system/Change.h
+++ b/include/xrpl/tx/transactors/system/Change.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/system/LedgerStateFix.h b/include/xrpl/tx/transactors/system/LedgerStateFix.h
index 973f89faa9..dd91f6f367 100644
--- a/include/xrpl/tx/transactors/system/LedgerStateFix.h
+++ b/include/xrpl/tx/transactors/system/LedgerStateFix.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class LedgerStateFix : public Transactor
diff --git a/include/xrpl/tx/transactors/system/TicketCreate.h b/include/xrpl/tx/transactors/system/TicketCreate.h
index 5783faa6d1..2a1036c732 100644
--- a/include/xrpl/tx/transactors/system/TicketCreate.h
+++ b/include/xrpl/tx/transactors/system/TicketCreate.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class TicketCreate : public Transactor
diff --git a/include/xrpl/tx/transactors/token/Clawback.h b/include/xrpl/tx/transactors/token/Clawback.h
index ed90776e59..5eaa32fe48 100644
--- a/include/xrpl/tx/transactors/token/Clawback.h
+++ b/include/xrpl/tx/transactors/token/Clawback.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h
index ff6f76e8ea..21d95326c9 100644
--- a/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h
index 2e2591844f..6de2ce141f 100644
--- a/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h
index cb4b6295a0..e2ee844560 100644
--- a/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h
index 585c273e12..7c348d36a5 100644
--- a/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h
index 72599ab987..aecf1ea5e2 100644
--- a/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h
+++ b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h
index e30d123e52..b9fd48abe3 100644
--- a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h
+++ b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h
@@ -1,7 +1,19 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 struct MPTAuthorizeArgs
diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h
index d946587e32..045911c7ae 100644
--- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h
+++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h
@@ -1,9 +1,22 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h
index 21032e7337..743e5a9502 100644
--- a/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h
+++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h
index 428c573e2f..52f155e8fe 100644
--- a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h
+++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class MPTokenIssuanceSet : public Transactor
diff --git a/include/xrpl/tx/transactors/token/TrustSet.h b/include/xrpl/tx/transactors/token/TrustSet.h
index d719f06326..fbf50f56ee 100644
--- a/include/xrpl/tx/transactors/token/TrustSet.h
+++ b/include/xrpl/tx/transactors/token/TrustSet.h
@@ -1,8 +1,18 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 class TrustSet : public Transactor
diff --git a/include/xrpl/tx/transactors/vault/VaultClawback.h b/include/xrpl/tx/transactors/vault/VaultClawback.h
index 2ff97abca2..ba0eb9f320 100644
--- a/include/xrpl/tx/transactors/vault/VaultClawback.h
+++ b/include/xrpl/tx/transactors/vault/VaultClawback.h
@@ -1,8 +1,18 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/include/xrpl/tx/transactors/vault/VaultCreate.h b/include/xrpl/tx/transactors/vault/VaultCreate.h
index 9b11f97957..f946a22c3a 100644
--- a/include/xrpl/tx/transactors/vault/VaultCreate.h
+++ b/include/xrpl/tx/transactors/vault/VaultCreate.h
@@ -1,7 +1,16 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 class VaultCreate : public Transactor
diff --git a/include/xrpl/tx/transactors/vault/VaultDelete.h b/include/xrpl/tx/transactors/vault/VaultDelete.h
index b8bb3c4096..ca73b1a685 100644
--- a/include/xrpl/tx/transactors/vault/VaultDelete.h
+++ b/include/xrpl/tx/transactors/vault/VaultDelete.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/vault/VaultDeposit.h b/include/xrpl/tx/transactors/vault/VaultDeposit.h
index 523b3f2e53..d49f504243 100644
--- a/include/xrpl/tx/transactors/vault/VaultDeposit.h
+++ b/include/xrpl/tx/transactors/vault/VaultDeposit.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/vault/VaultSet.h b/include/xrpl/tx/transactors/vault/VaultSet.h
index 5c362d5db4..835ed69610 100644
--- a/include/xrpl/tx/transactors/vault/VaultSet.h
+++ b/include/xrpl/tx/transactors/vault/VaultSet.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h b/include/xrpl/tx/transactors/vault/VaultWithdraw.h
index 7bbe06187d..22ad39d26d 100644
--- a/include/xrpl/tx/transactors/vault/VaultWithdraw.h
+++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/libxrpl/core/detail/JobQueue.cpp b/src/libxrpl/core/detail/JobQueue.cpp
index 5c07bf68cc..ffb91db72d 100644
--- a/src/libxrpl/core/detail/JobQueue.cpp
+++ b/src/libxrpl/core/detail/JobQueue.cpp
@@ -6,6 +6,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp
index 93875f497a..074a88428c 100644
--- a/src/libxrpl/json/json_value.cpp
+++ b/src/libxrpl/json/json_value.cpp
@@ -1,9 +1,11 @@
 #include 
 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 
diff --git a/src/libxrpl/net/RegisterSSLCerts.cpp b/src/libxrpl/net/RegisterSSLCerts.cpp
index ff59b5971e..2f005554a0 100644
--- a/src/libxrpl/net/RegisterSSLCerts.cpp
+++ b/src/libxrpl/net/RegisterSSLCerts.cpp
@@ -6,6 +6,8 @@
 #include 
 
 #if BOOST_OS_WINDOWS
+#include 
+
 #include 
 #include 
 
diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp
index e58ab29257..52b7fe82c2 100644
--- a/src/libxrpl/protocol/AMMCore.cpp
+++ b/src/libxrpl/protocol/AMMCore.cpp
@@ -12,6 +12,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp
index 0baff1e33a..fe8a08c2ef 100644
--- a/src/libxrpl/protocol/ConfidentialTransfer.cpp
+++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp
index 80aa1b1f4e..2f3e25f823 100644
--- a/src/libxrpl/protocol/Permissions.cpp
+++ b/src/libxrpl/protocol/Permissions.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp
index 8ef7b9760f..fcc0077f6b 100644
--- a/src/libxrpl/protocol/STNumber.cpp
+++ b/src/libxrpl/protocol/STNumber.cpp
@@ -5,7 +5,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/server/State.cpp b/src/libxrpl/server/State.cpp
index d9793f53d0..ce17505ee6 100644
--- a/src/libxrpl/server/State.cpp
+++ b/src/libxrpl/server/State.cpp
@@ -7,6 +7,7 @@
 
 #include   // IWYU pragma: keep
 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp
index f3ae9dc925..f3a7ff76ba 100644
--- a/src/libxrpl/server/Wallet.cpp
+++ b/src/libxrpl/server/Wallet.cpp
@@ -19,7 +19,9 @@
 #include 
 #include   // IWYU pragma: keep
 
+#include   // IWYU pragma: keep
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp
index 544a3af2dc..fcb53e7c8f 100644
--- a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp
+++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
index ed5a22e3db..a915bb60d1 100644
--- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
+++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp
index 1948f3803d..259b9d7864 100644
--- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp
+++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp
index 16d27f98d9..ba54c14b1e 100644
--- a/src/libxrpl/tx/transactors/system/Batch.cpp
+++ b/src/libxrpl/tx/transactors/system/Batch.cpp
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp
index 2e224a7eb1..39d722da65 100644
--- a/src/test/app/FlowMPT_test.cpp
+++ b/src/test/app/FlowMPT_test.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp
index da98c03380..bac1d8b61f 100644
--- a/src/test/app/LedgerReplay_test.cpp
+++ b/src/test/app/LedgerReplay_test.cpp
@@ -12,6 +12,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp
index 8fb1eb31ab..5f02f1b079 100644
--- a/src/test/app/MultiSign_test.cpp
+++ b/src/test/app/MultiSign_test.cpp
@@ -35,6 +35,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp
index 9958515c5f..800b14877a 100644
--- a/src/test/app/OfferMPT_test.cpp
+++ b/src/test/app/OfferMPT_test.cpp
@@ -12,11 +12,13 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp
index 1f9290de63..11a0e5bab0 100644
--- a/src/test/app/PayStrand_test.cpp
+++ b/src/test/app/PayStrand_test.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 #include   // IWYU pragma: keep
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp
index 7a149b2f64..c219bd8737 100644
--- a/src/test/app/SHAMapStore_test.cpp
+++ b/src/test/app/SHAMapStore_test.cpp
@@ -15,6 +15,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/basics/hardened_hash_test.cpp b/src/test/basics/hardened_hash_test.cpp
index 885ba9d60b..8b10967932 100644
--- a/src/test/basics/hardened_hash_test.cpp
+++ b/src/test/basics/hardened_hash_test.cpp
@@ -1,4 +1,5 @@
 #include 
+#include 
 #include 
 
 #include 
diff --git a/src/test/beast/IPEndpointCommon.h b/src/test/beast/IPEndpointCommon.h
index 0ff0da35be..45d036476c 100644
--- a/src/test/beast/IPEndpointCommon.h
+++ b/src/test/beast/IPEndpointCommon.h
@@ -1,8 +1,12 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
 
+#include 
+
 namespace beast::IP {
 
 inline Endpoint
diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp
index 413194491a..3dbaf74040 100644
--- a/src/test/beast/aged_associative_container_test.cpp
+++ b/src/test/beast/aged_associative_container_test.cpp
@@ -1,12 +1,12 @@
 #include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
+#include                  // IWYU pragma: keep
+#include             // IWYU pragma: keep
+#include             // IWYU pragma: keep
+#include                  // IWYU pragma: keep
+#include        // IWYU pragma: keep
+#include   // IWYU pragma: keep
+#include   // IWYU pragma: keep
+#include        // IWYU pragma: keep
 #include 
 #include 
 #include 
@@ -18,9 +18,9 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
-#include 
+#include   // IWYU pragma: keep
 
 #ifndef BEAST_AGED_UNORDERED_NO_ALLOC_DEFAULTCTOR
 #ifdef _MSC_VER
diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp
index 57ff19fec5..d37569d6cd 100644
--- a/src/test/core/SociDB_test.cpp
+++ b/src/test/core/SociDB_test.cpp
@@ -11,6 +11,7 @@
 #include 
 #include   // IWYU pragma: keep
 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/test/csf.h b/src/test/csf.h
deleted file mode 100644
index d2ddbb460d..0000000000
--- a/src/test/csf.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
diff --git a/src/test/csf/BasicNetwork.h b/src/test/csf/BasicNetwork.h
index 418cdcf289..4b88592128 100644
--- a/src/test/csf/BasicNetwork.h
+++ b/src/test/csf/BasicNetwork.h
@@ -3,6 +3,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl::test::csf {
 /** Peer to peer network simulator.
 
diff --git a/src/test/csf/CollectorRef.h b/src/test/csf/CollectorRef.h
index 23baa020b8..5bc55c7515 100644
--- a/src/test/csf/CollectorRef.h
+++ b/src/test/csf/CollectorRef.h
@@ -1,7 +1,14 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl::test::csf {
 
diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h
index 20e45faa5b..82ed713561 100644
--- a/src/test/csf/Digraph.h
+++ b/src/test/csf/Digraph.h
@@ -4,10 +4,11 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
-#include 
-#include 
+#include 
+#include 
 
 namespace xrpl {
 namespace detail {
diff --git a/src/test/csf/Histogram.h b/src/test/csf/Histogram.h
index 84f1cc7b72..60cb9a132e 100644
--- a/src/test/csf/Histogram.h
+++ b/src/test/csf/Histogram.h
@@ -1,9 +1,9 @@
 #pragma once
 
-#include 
 #include 
-#include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl::test::csf {
diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h
index 3e9eed1c52..9d29704172 100644
--- a/src/test/csf/Peer.h
+++ b/src/test/csf/Peer.h
@@ -2,7 +2,9 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -10,15 +12,31 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
 
 #include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::csf {
 
diff --git a/src/test/csf/PeerGroup.h b/src/test/csf/PeerGroup.h
index 01bba82c98..e5efecac34 100644
--- a/src/test/csf/PeerGroup.h
+++ b/src/test/csf/PeerGroup.h
@@ -1,9 +1,18 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl::test::csf {
diff --git a/src/test/csf/Scheduler.h b/src/test/csf/Scheduler.h
index e7cbe27036..3fc187e1e7 100644
--- a/src/test/csf/Scheduler.h
+++ b/src/test/csf/Scheduler.h
@@ -6,6 +6,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h
index b5b7f5f9c2..d2a63f6507 100644
--- a/src/test/csf/Sim.h
+++ b/src/test/csf/Sim.h
@@ -2,16 +2,22 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
+#include 
+
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl::test::csf {
 
diff --git a/src/test/csf/TrustGraph.h b/src/test/csf/TrustGraph.h
index 202d17a6d5..a10e318706 100644
--- a/src/test/csf/TrustGraph.h
+++ b/src/test/csf/TrustGraph.h
@@ -1,13 +1,11 @@
 #pragma once
 
 #include 
-#include 
 
 #include 
 
-#include 
-#include 
-#include 
+#include 
+#include 
 #include 
 
 namespace xrpl::test::csf {
diff --git a/src/test/csf/Tx.h b/src/test/csf/Tx.h
index 5a0f6a9bae..f871909f48 100644
--- a/src/test/csf/Tx.h
+++ b/src/test/csf/Tx.h
@@ -7,6 +7,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/csf/Validation.h b/src/test/csf/Validation.h
index 81f14efa18..73b6784534 100644
--- a/src/test/csf/Validation.h
+++ b/src/test/csf/Validation.h
@@ -2,10 +2,12 @@
 
 #include 
 
+#include 
 #include 
 
-#include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl::test::csf {
diff --git a/src/test/csf/collectors.h b/src/test/csf/collectors.h
index 6dcbd65a1e..1100f6c690 100644
--- a/src/test/csf/collectors.h
+++ b/src/test/csf/collectors.h
@@ -2,14 +2,24 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl::test::csf {
 
diff --git a/src/test/csf/events.h b/src/test/csf/events.h
index ea163d3a80..dfedd2627a 100644
--- a/src/test/csf/events.h
+++ b/src/test/csf/events.h
@@ -1,12 +1,9 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
 
-#include 
-
 namespace xrpl::test::csf {
 
 // Events are emitted by peers at a variety of points during the simulation.
@@ -77,7 +74,7 @@ struct SubmitTx
 struct StartRound
 {
     //! The preferred ledger for the start of consensus
-    Ledger::ID bestLedger;
+    Ledger::ID bestLedger{};
 
     //! The prior ledger on hand
     Ledger prevLedger;
diff --git a/src/test/csf/ledgers.h b/src/test/csf/ledgers.h
index 25672de133..dc5ec5277c 100644
--- a/src/test/csf/ledgers.h
+++ b/src/test/csf/ledgers.h
@@ -2,7 +2,6 @@
 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -11,8 +10,15 @@
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::csf {
 
diff --git a/src/test/csf/random.h b/src/test/csf/random.h
index c506397d88..9fc51f1217 100644
--- a/src/test/csf/random.h
+++ b/src/test/csf/random.h
@@ -1,5 +1,8 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/src/test/csf/submitters.h b/src/test/csf/submitters.h
index a5c494beb0..546681abe0 100644
--- a/src/test/csf/submitters.h
+++ b/src/test/csf/submitters.h
@@ -1,10 +1,11 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl::test::csf {
diff --git a/src/test/csf/timers.h b/src/test/csf/timers.h
index 2f2ec4dc93..2f86fe7729 100644
--- a/src/test/csf/timers.h
+++ b/src/test/csf/timers.h
@@ -4,6 +4,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl::test::csf {
diff --git a/src/test/json/TestOutputSuite.h b/src/test/json/TestOutputSuite.h
index e954a043b8..1be2a72c6d 100644
--- a/src/test/json/TestOutputSuite.h
+++ b/src/test/json/TestOutputSuite.h
@@ -5,8 +5,10 @@
 #include 
 #include 
 
-namespace xrpl {
-namespace test {
+#include 
+#include 
+
+namespace xrpl::test {
 
 class TestOutputSuite : public TestSuite
 {
@@ -32,5 +34,4 @@ protected:
     }
 };
 
-}  // namespace test
-}  // namespace xrpl
+}  // namespace xrpl::test
diff --git a/src/test/jtx.h b/src/test/jtx.h
deleted file mode 100644
index d4b88b0b9e..0000000000
--- a/src/test/jtx.h
+++ /dev/null
@@ -1,60 +0,0 @@
-#pragma once
-
-// Convenience header that includes everything
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h
index 99131637bb..605f15f812 100644
--- a/src/test/jtx/AMM.h
+++ b/src/test/jtx/AMM.h
@@ -6,14 +6,33 @@
 #include 
 #include 
 
-#include 
-
+#include 
+#include 
+#include 
 #include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::test::jtx {
 
 class LPToken
diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h
index a2e6c38db3..971cc5db84 100644
--- a/src/test/jtx/AMMTest.h
+++ b/src/test/jtx/AMMTest.h
@@ -1,11 +1,23 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 
 #include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/AbstractClient.h b/src/test/jtx/AbstractClient.h
index 1eb51f2123..7c7107ca79 100644
--- a/src/test/jtx/AbstractClient.h
+++ b/src/test/jtx/AbstractClient.h
@@ -2,6 +2,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl::test {
 
 /* Abstract XRPL client interface.
diff --git a/src/test/jtx/Account.h b/src/test/jtx/Account.h
index b20b4a359a..264a39f08b 100644
--- a/src/test/jtx/Account.h
+++ b/src/test/jtx/Account.h
@@ -1,12 +1,14 @@
 #pragma once
 
 #include 
+#include 
 #include 
+#include 
 #include 
-#include 
 
 #include 
 #include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/CaptureLogs.h b/src/test/jtx/CaptureLogs.h
index b5fe302629..38db19219a 100644
--- a/src/test/jtx/CaptureLogs.h
+++ b/src/test/jtx/CaptureLogs.h
@@ -1,6 +1,12 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/test/jtx/CheckMessageLogs.h b/src/test/jtx/CheckMessageLogs.h
index 4bbdec6f06..fc46f41671 100644
--- a/src/test/jtx/CheckMessageLogs.h
+++ b/src/test/jtx/CheckMessageLogs.h
@@ -1,6 +1,11 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h
index b758683da6..404ddbe31d 100644
--- a/src/test/jtx/ConfidentialTransfer.h
+++ b/src/test/jtx/ConfidentialTransfer.h
@@ -1,19 +1,8 @@
 #pragma once
 
-#include 
 #include 
 #include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
 #include 
-#include 
-#include 
-#include 
 #include 
 
 #include 
@@ -22,21 +11,10 @@
 #include 
 #include 
 #include 
-#include 
-#include 
-#include 
-#include 
 #include 
-#include 
-#include 
-#include 
 #include 
-#include 
-#include 
-#include 
 #include 
 #include 
-#include 
 
 #include 
 
diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h
index aca6074c4a..ba28d19738 100644
--- a/src/test/jtx/Env.h
+++ b/src/test/jtx/Env.h
@@ -7,7 +7,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 
@@ -17,25 +16,39 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
-#include 
-#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
-#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
-#include 
-#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/jtx/Env_ss.h b/src/test/jtx/Env_ss.h
index ca0825eac7..16e1cdbc82 100644
--- a/src/test/jtx/Env_ss.h
+++ b/src/test/jtx/Env_ss.h
@@ -1,6 +1,12 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
+
+#include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h
index 121e6cc825..da105e01aa 100644
--- a/src/test/jtx/JTx.h
+++ b/src/test/jtx/JTx.h
@@ -10,6 +10,9 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/Oracle.h b/src/test/jtx/Oracle.h
index cf091c6d13..2c296ba705 100644
--- a/src/test/jtx/Oracle.h
+++ b/src/test/jtx/Oracle.h
@@ -1,8 +1,25 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::jtx::oracle {
 
diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h
index a391adcb1b..fa3f0d40f9 100644
--- a/src/test/jtx/PathSet.h
+++ b/src/test/jtx/PathSet.h
@@ -1,10 +1,20 @@
 #pragma once
 
-#include 
+#include 
+#include 
 
-#include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h
index 34532b17f7..b1643de362 100644
--- a/src/test/jtx/TestHelpers.h
+++ b/src/test/jtx/TestHelpers.h
@@ -1,20 +1,51 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/src/test/jtx/TestSuite.h b/src/test/jtx/TestSuite.h
index 705adbb620..d40cc6aba6 100644
--- a/src/test/jtx/TestSuite.h
+++ b/src/test/jtx/TestSuite.h
@@ -1,7 +1,9 @@
 #pragma once
 
-#include 
+#include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h
index 9a53a32481..008bbe70c3 100644
--- a/src/test/jtx/TrustedPublisherServer.h
+++ b/src/test/jtx/TrustedPublisherServer.h
@@ -2,11 +2,18 @@
 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
@@ -19,9 +26,21 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/test/jtx/WSClient.h b/src/test/jtx/WSClient.h
index 5a22fafe2e..4aa41e072c 100644
--- a/src/test/jtx/WSClient.h
+++ b/src/test/jtx/WSClient.h
@@ -4,9 +4,14 @@
 
 #include 
 
+#include 
+
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/test/jtx/account_txn_id.h b/src/test/jtx/account_txn_id.h
index 89f7368ee6..ffee4123aa 100644
--- a/src/test/jtx/account_txn_id.h
+++ b/src/test/jtx/account_txn_id.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h
index 5c8a136617..3f8b9f1ed2 100644
--- a/src/test/jtx/acctdelete.h
+++ b/src/test/jtx/acctdelete.h
@@ -3,7 +3,9 @@
 #include 
 #include 
 
-#include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h
index db281419ef..ce0234b41a 100644
--- a/src/test/jtx/amount.h
+++ b/src/test/jtx/amount.h
@@ -3,14 +3,26 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/jtx/balance.h b/src/test/jtx/balance.h
index 5ea80c77f8..3f39e9f0fb 100644
--- a/src/test/jtx/balance.h
+++ b/src/test/jtx/balance.h
@@ -1,8 +1,12 @@
 #pragma once
 
+#include 
 #include 
+#include 
 #include 
 
+#include 
+
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h
index 4d564f150a..bb1deec1a2 100644
--- a/src/test/jtx/batch.h
+++ b/src/test/jtx/batch.h
@@ -2,17 +2,21 @@
 
 #include 
 #include 
+#include 
 #include 
-#include 
-#include 
-#include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 /** @brief Helpers for constructing Batch test transactions. */
 namespace xrpl::test::jtx::batch {
diff --git a/src/test/jtx/check.h b/src/test/jtx/check.h
index 7c3dc6aa80..f66d802247 100644
--- a/src/test/jtx/check.h
+++ b/src/test/jtx/check.h
@@ -1,9 +1,13 @@
 #pragma once
 
 #include 
-#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/credentials.h b/src/test/jtx/credentials.h
index 4bdd716918..8da2f289a1 100644
--- a/src/test/jtx/credentials.h
+++ b/src/test/jtx/credentials.h
@@ -2,7 +2,18 @@
 
 #include 
 #include 
-#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::jtx::credentials {
 
diff --git a/src/test/jtx/delegate.h b/src/test/jtx/delegate.h
index ae848603a7..9b64c105ba 100644
--- a/src/test/jtx/delegate.h
+++ b/src/test/jtx/delegate.h
@@ -2,8 +2,14 @@
 
 #include 
 #include 
+#include 
 
+#include 
+#include 
+
+#include 
 #include 
+#include 
 
 namespace xrpl::test::jtx::delegate {
 
diff --git a/src/test/jtx/delivermin.h b/src/test/jtx/delivermin.h
index 0a8fa9f823..29256e37bd 100644
--- a/src/test/jtx/delivermin.h
+++ b/src/test/jtx/delivermin.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 
 #include 
 
diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h
index 0d8e363138..5ba12648e2 100644
--- a/src/test/jtx/deposit.h
+++ b/src/test/jtx/deposit.h
@@ -1,7 +1,14 @@
 #pragma once
 
 #include 
-#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
 
 /** Deposit preauthorize operations */
 namespace xrpl::test::jtx::deposit {
diff --git a/src/test/jtx/did.h b/src/test/jtx/did.h
index 7662c6556a..30c89fd879 100644
--- a/src/test/jtx/did.h
+++ b/src/test/jtx/did.h
@@ -2,7 +2,13 @@
 
 #include 
 #include 
-#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
 
 /** DID operations. */
 namespace xrpl::test::jtx::did {
diff --git a/src/test/jtx/directory.h b/src/test/jtx/directory.h
index c63640ae1f..13473f949e 100644
--- a/src/test/jtx/directory.h
+++ b/src/test/jtx/directory.h
@@ -2,11 +2,15 @@
 
 #include 
 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
 
 #include 
 #include 
+#include 
 #include 
 
 /** Directory operations. */
diff --git a/src/test/jtx/domain.h b/src/test/jtx/domain.h
index b3a0744768..ebcfdb662a 100644
--- a/src/test/jtx/domain.h
+++ b/src/test/jtx/domain.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h
index 4c56ec8217..0079605637 100644
--- a/src/test/jtx/envconfig.h
+++ b/src/test/jtx/envconfig.h
@@ -2,6 +2,11 @@
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::test {
 
 extern std::atomic gEnvUseIPv4;
diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h
index 58be9b701a..728b2e3643 100644
--- a/src/test/jtx/escrow.h
+++ b/src/test/jtx/escrow.h
@@ -3,10 +3,15 @@
 #include 
 #include 
 #include 
-#include 
-#include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
 
 /** Escrow operations. */
 namespace xrpl::test::jtx::escrow {
diff --git a/src/test/jtx/fee.h b/src/test/jtx/fee.h
index 048b262e88..ad3002dc75 100644
--- a/src/test/jtx/fee.h
+++ b/src/test/jtx/fee.h
@@ -1,12 +1,15 @@
 #pragma once
 
 #include 
+#include 
 #include 
 
 #include 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h
index 6e0c49b752..83470f9a1b 100644
--- a/src/test/jtx/flags.h
+++ b/src/test/jtx/flags.h
@@ -1,11 +1,15 @@
 #pragma once
 
+#include 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp
index 991c84fc18..cd96ef5c0d 100644
--- a/src/test/jtx/impl/Oracle.cpp
+++ b/src/test/jtx/impl/Oracle.cpp
@@ -13,6 +13,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/test/jtx/invoice_id.h b/src/test/jtx/invoice_id.h
index 2246d54abf..b2f13a1d96 100644
--- a/src/test/jtx/invoice_id.h
+++ b/src/test/jtx/invoice_id.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/jtx_json.h b/src/test/jtx/jtx_json.h
index d58f0b47ac..b22d4c30e1 100644
--- a/src/test/jtx/jtx_json.h
+++ b/src/test/jtx/jtx_json.h
@@ -1,9 +1,12 @@
 #pragma once
 
 #include 
+#include 
 
 #include 
 
+#include 
+
 namespace xrpl::test::jtx {
 
 /** Inject raw JSON. */
diff --git a/src/test/jtx/last_ledger_sequence.h b/src/test/jtx/last_ledger_sequence.h
index 6170c2e4e1..ff6390298e 100644
--- a/src/test/jtx/last_ledger_sequence.h
+++ b/src/test/jtx/last_ledger_sequence.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/ledgerStateFix.h b/src/test/jtx/ledgerStateFix.h
index 71f3f76101..dd1ac19f04 100644
--- a/src/test/jtx/ledgerStateFix.h
+++ b/src/test/jtx/ledgerStateFix.h
@@ -1,7 +1,9 @@
 #pragma once
 
 #include 
-#include 
+
+#include 
+#include 
 
 /** LedgerStateFix operations. */
 namespace xrpl::test::jtx::ledgerStateFix {
diff --git a/src/test/jtx/memo.h b/src/test/jtx/memo.h
index 9b5c1118ac..cea4923d38 100644
--- a/src/test/jtx/memo.h
+++ b/src/test/jtx/memo.h
@@ -1,7 +1,9 @@
 #pragma once
 
 #include 
+#include 
 
+#include 
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h
index d5fba82e08..1a7fe94785 100644
--- a/src/test/jtx/mpt.h
+++ b/src/test/jtx/mpt.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -9,14 +10,33 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h
index 3fef2ab446..c90e28537a 100644
--- a/src/test/jtx/multisign.h
+++ b/src/test/jtx/multisign.h
@@ -1,14 +1,22 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
-#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 #include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/noop.h b/src/test/jtx/noop.h
index 095e20f387..c38dfdde25 100644
--- a/src/test/jtx/noop.h
+++ b/src/test/jtx/noop.h
@@ -1,7 +1,10 @@
 #pragma once
 
+#include 
 #include 
 
+#include 
+
 namespace xrpl::test::jtx {
 
 /** The null transaction. */
diff --git a/src/test/jtx/offer.h b/src/test/jtx/offer.h
index 9a31904058..f3e7277933 100644
--- a/src/test/jtx/offer.h
+++ b/src/test/jtx/offer.h
@@ -5,6 +5,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl::test::jtx {
 
 /** Create an offer. */
diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h
index 44fd3762c2..0fb38b407c 100644
--- a/src/test/jtx/owners.h
+++ b/src/test/jtx/owners.h
@@ -1,10 +1,11 @@
 #pragma once
 
+#include 
 #include 
 
 #include 
+#include 
 #include 
-#include 
 
 #include 
 #include 
diff --git a/src/test/jtx/paths.h b/src/test/jtx/paths.h
index a8a8f7900c..1450985687 100644
--- a/src/test/jtx/paths.h
+++ b/src/test/jtx/paths.h
@@ -1,8 +1,13 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
 
-#include 
+#include 
+#include 
+#include 
 
 #include 
 
diff --git a/src/test/jtx/pay.h b/src/test/jtx/pay.h
index 8f4f3287ae..fa193e7b02 100644
--- a/src/test/jtx/pay.h
+++ b/src/test/jtx/pay.h
@@ -4,6 +4,7 @@
 #include 
 
 #include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/permissioned_dex.h b/src/test/jtx/permissioned_dex.h
index b0a4d4e2d5..a8afcac2eb 100644
--- a/src/test/jtx/permissioned_dex.h
+++ b/src/test/jtx/permissioned_dex.h
@@ -3,6 +3,11 @@
 #include 
 #include 
 
+#include 
+
+#include 
+#include 
+
 namespace xrpl::test::jtx {
 
 uint256
diff --git a/src/test/jtx/permissioned_domains.h b/src/test/jtx/permissioned_domains.h
index fdd2b0da1d..f4dc080cbf 100644
--- a/src/test/jtx/permissioned_domains.h
+++ b/src/test/jtx/permissioned_domains.h
@@ -4,6 +4,18 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::test::jtx::pdomain {
 
 // Helpers for PermissionedDomains testing
diff --git a/src/test/jtx/prop.h b/src/test/jtx/prop.h
index c3ec6bc0fe..e85751e4c9 100644
--- a/src/test/jtx/prop.h
+++ b/src/test/jtx/prop.h
@@ -1,6 +1,8 @@
 #pragma once
 
 #include 
+#include 
+#include 
 
 #include 
 
diff --git a/src/test/jtx/quality.h b/src/test/jtx/quality.h
index 75bf930434..c7896fadf9 100644
--- a/src/test/jtx/quality.h
+++ b/src/test/jtx/quality.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/require.h b/src/test/jtx/require.h
index 91cc75b3f5..19e161b938 100644
--- a/src/test/jtx/require.h
+++ b/src/test/jtx/require.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include 
 #include 
 
 #include 
diff --git a/src/test/jtx/rpc.h b/src/test/jtx/rpc.h
index 1f538f9ca5..bc05450909 100644
--- a/src/test/jtx/rpc.h
+++ b/src/test/jtx/rpc.h
@@ -1,8 +1,13 @@
 #pragma once
 
 #include 
+#include 
 
-#include 
+#include 
+#include 
+
+#include 
+#include 
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/sendmax.h b/src/test/jtx/sendmax.h
index 125b10b7d4..1241d76b91 100644
--- a/src/test/jtx/sendmax.h
+++ b/src/test/jtx/sendmax.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 
 #include 
 
diff --git a/src/test/jtx/seq.h b/src/test/jtx/seq.h
index de06ac3fbc..956c0a77e8 100644
--- a/src/test/jtx/seq.h
+++ b/src/test/jtx/seq.h
@@ -1,8 +1,10 @@
 #pragma once
 
 #include 
+#include 
 #include 
 
+#include 
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h
index e88785ef15..76f0a34dff 100644
--- a/src/test/jtx/sig.h
+++ b/src/test/jtx/sig.h
@@ -1,6 +1,11 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
+
+#include 
 
 #include 
 
diff --git a/src/test/jtx/tag.h b/src/test/jtx/tag.h
index 9279e2a13b..77870367a9 100644
--- a/src/test/jtx/tag.h
+++ b/src/test/jtx/tag.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/ter.h b/src/test/jtx/ter.h
index c410b46a1e..880711dca0 100644
--- a/src/test/jtx/ter.h
+++ b/src/test/jtx/ter.h
@@ -1,7 +1,11 @@
 #pragma once
 
 #include 
+#include 
 
+#include 
+
+#include 
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/ticket.h b/src/test/jtx/ticket.h
index 02f742ee93..1035be7674 100644
--- a/src/test/jtx/ticket.h
+++ b/src/test/jtx/ticket.h
@@ -2,8 +2,12 @@
 
 #include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/token.h b/src/test/jtx/token.h
index 206d170534..97f968fdfc 100644
--- a/src/test/jtx/token.h
+++ b/src/test/jtx/token.h
@@ -2,11 +2,17 @@
 
 #include 
 #include 
-#include 
+#include 
 
+#include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl::test::jtx::token {
 
diff --git a/src/test/jtx/trust.h b/src/test/jtx/trust.h
index 07fd0f7c4e..034f80fcec 100644
--- a/src/test/jtx/trust.h
+++ b/src/test/jtx/trust.h
@@ -5,6 +5,9 @@
 #include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl::test::jtx {
 
 /** Modify a trust line. */
diff --git a/src/test/jtx/txflags.h b/src/test/jtx/txflags.h
index 975038d26a..7f5b31b2ac 100644
--- a/src/test/jtx/txflags.h
+++ b/src/test/jtx/txflags.h
@@ -1,6 +1,9 @@
 #pragma once
 
 #include 
+#include 
+
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h
index c3ed91f672..f1cf3f7ae8 100644
--- a/src/test/jtx/utility.h
+++ b/src/test/jtx/utility.h
@@ -2,11 +2,13 @@
 
 #include 
 
+#include 
 #include 
-#include 
+#include 
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl::test::jtx {
diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h
index c1e0831a66..4e6b90fe1f 100644
--- a/src/test/jtx/vault.h
+++ b/src/test/jtx/vault.h
@@ -1,13 +1,13 @@
 #pragma once
 
 #include 
-#include 
 
 #include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/src/test/jtx/xchain_bridge.h b/src/test/jtx/xchain_bridge.h
index e4c012510d..aacef80bbe 100644
--- a/src/test/jtx/xchain_bridge.h
+++ b/src/test/jtx/xchain_bridge.h
@@ -1,12 +1,19 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 
 #include 
+#include 
+#include 
 #include 
-#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::test::jtx {
 
diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h
index 885c3bbeac..1254fc51aa 100644
--- a/src/test/nodestore/TestBase.h
+++ b/src/test/nodestore/TestBase.h
@@ -1,17 +1,22 @@
 #pragma once
 
-#include 
+#include 
+#include 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::NodeStore {
 
diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp
index 801eebbe63..b823b24962 100644
--- a/src/test/protocol/STObject_test.cpp
+++ b/src/test/protocol/STObject_test.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp
index bc5c72b4fc..24981053f5 100644
--- a/src/test/protocol/STParsedJSON_test.cpp
+++ b/src/test/protocol/STParsedJSON_test.cpp
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/test/rpc/GRPCTestClientBase.h b/src/test/rpc/GRPCTestClientBase.h
deleted file mode 100644
index 15d335781d..0000000000
--- a/src/test/rpc/GRPCTestClientBase.h
+++ /dev/null
@@ -1,32 +0,0 @@
-#pragma once
-
-#include 
-
-#include 
-
-#include 
-
-namespace xrpl {
-namespace test {
-
-struct GRPCTestClientBase
-{
-    explicit GRPCTestClientBase(std::string const& port)
-        : stub(
-              org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
-                  grpc::CreateChannel(
-                      beast::IP::Endpoint(
-                          boost::asio::ip::make_address(getEnvLocalhostAddr()),
-                          std::stoi(port))
-                          .to_string(),
-                      grpc::InsecureChannelCredentials())))
-    {
-    }
-
-    grpc::Status status;
-    grpc::ClientContext context;
-    std::unique_ptr stub;
-};
-
-}  // namespace test
-}  // namespace xrpl
diff --git a/src/test/shamap/common.h b/src/test/shamap/common.h
index 0475acdf6d..9bef10d851 100644
--- a/src/test/shamap/common.h
+++ b/src/test/shamap/common.h
@@ -1,11 +1,24 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::tests {
 
diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h
index 19110469ee..3fc5d015b0 100644
--- a/src/test/unit_test/FileDirGuard.h
+++ b/src/test/unit_test/FileDirGuard.h
@@ -1,12 +1,16 @@
 #pragma once
 
-#include 
-
 #include 
+#include 
 
 #include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::detail {
 
diff --git a/src/test/unit_test/SuiteJournal.h b/src/test/unit_test/SuiteJournal.h
index 174e9ff2ff..66287e328c 100644
--- a/src/test/unit_test/SuiteJournal.h
+++ b/src/test/unit_test/SuiteJournal.h
@@ -1,8 +1,13 @@
 #pragma once
 
-#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::test {
 
 // A Journal::Sink intended for use with the beast unit test framework.
diff --git a/src/test/unit_test/multi_runner.h b/src/test/unit_test/multi_runner.h
index 55cf6c25fa..3390014cb8 100644
--- a/src/test/unit_test/multi_runner.h
+++ b/src/test/unit_test/multi_runner.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -12,11 +13,15 @@
 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
-#include 
 #include 
 
 namespace xrpl {
diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt
index bd56028728..df483a36d8 100644
--- a/src/tests/libxrpl/CMakeLists.txt
+++ b/src/tests/libxrpl/CMakeLists.txt
@@ -1,5 +1,6 @@
 include(GoogleTest)
 include(isolate_headers)
+include(verify_headers)
 
 # Test requirements.
 find_package(GTest REQUIRED)
@@ -54,4 +55,10 @@ foreach(module IN LISTS test_modules)
     )
 endforeach()
 
+# The test helpers and per-module test headers are not built with add_module,
+# so verify them against the test binary's own compile environment.
+if(verify_headers)
+    verify_target_headers(xrpl_tests "${CMAKE_CURRENT_SOURCE_DIR}")
+endif()
+
 gtest_discover_tests(xrpl_tests DISCOVERY_TIMEOUT 60)
diff --git a/src/tests/libxrpl/helpers/IOU.h b/src/tests/libxrpl/helpers/IOU.h
index d80f962edf..10deaee99b 100644
--- a/src/tests/libxrpl/helpers/IOU.h
+++ b/src/tests/libxrpl/helpers/IOU.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -8,7 +9,6 @@
 
 #include 
 
-#include 
 #include 
 #include 
 #include 
diff --git a/src/tests/libxrpl/helpers/TestFamily.h b/src/tests/libxrpl/helpers/TestFamily.h
index 50f514480a..a5ff34a111 100644
--- a/src/tests/libxrpl/helpers/TestFamily.h
+++ b/src/tests/libxrpl/helpers/TestFamily.h
@@ -1,13 +1,22 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h
index 0536176344..0108f886d3 100644
--- a/src/tests/libxrpl/helpers/TestServiceRegistry.h
+++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h
@@ -1,7 +1,9 @@
 #pragma once
 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -13,8 +15,11 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl::test {
 
diff --git a/src/tests/libxrpl/helpers/TestSink.h b/src/tests/libxrpl/helpers/TestSink.h
index 28c85f00e4..b8637b7038 100644
--- a/src/tests/libxrpl/helpers/TestSink.h
+++ b/src/tests/libxrpl/helpers/TestSink.h
@@ -2,6 +2,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl {
 class TestSink : public beast::Journal::Sink
 {
diff --git a/src/tests/libxrpl/helpers/TxTest.h b/src/tests/libxrpl/helpers/TxTest.h
index 1b3ce460a2..cb75cd5ee0 100644
--- a/src/tests/libxrpl/helpers/TxTest.h
+++ b/src/tests/libxrpl/helpers/TxTest.h
@@ -1,18 +1,23 @@
 #pragma once
 
 #include 
+#include 
 #include 
-#include 
+#include 
 #include 
-#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -24,6 +29,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/tests/libxrpl/protocol_autogen/TestHelpers.h b/src/tests/libxrpl/protocol_autogen/TestHelpers.h
index a32ab5e20c..0fc26032f0 100644
--- a/src/tests/libxrpl/protocol_autogen/TestHelpers.h
+++ b/src/tests/libxrpl/protocol_autogen/TestHelpers.h
@@ -9,6 +9,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/consensus/RCLCensorshipDetector.h b/src/xrpld/app/consensus/RCLCensorshipDetector.h
index 48df7368d9..4318a7b4ff 100644
--- a/src/xrpld/app/consensus/RCLCensorshipDetector.h
+++ b/src/xrpld/app/consensus/RCLCensorshipDetector.h
@@ -1,9 +1,9 @@
 #pragma once
 
 #include 
-#include 
 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/app/consensus/RCLConsensus.h b/src/xrpld/app/consensus/RCLConsensus.h
index 32759300fa..359f6b8009 100644
--- a/src/xrpld/app/consensus/RCLConsensus.h
+++ b/src/xrpld/app/consensus/RCLConsensus.h
@@ -4,21 +4,37 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+
+#include 
 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/consensus/RCLCxLedger.h b/src/xrpld/app/consensus/RCLCxLedger.h
index 9f7984aaa6..89f70f9add 100644
--- a/src/xrpld/app/consensus/RCLCxLedger.h
+++ b/src/xrpld/app/consensus/RCLCxLedger.h
@@ -2,10 +2,16 @@
 
 #include 
 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Represents a ledger in RCLConsensus.
diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.h b/src/xrpld/app/consensus/RCLCxPeerPos.h
index 4618bdb746..e73ac3b532 100644
--- a/src/xrpld/app/consensus/RCLCxPeerPos.h
+++ b/src/xrpld/app/consensus/RCLCxPeerPos.h
@@ -2,11 +2,14 @@
 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
diff --git a/src/xrpld/app/consensus/RCLCxTx.h b/src/xrpld/app/consensus/RCLCxTx.h
index 52637d32b3..f174a2fd54 100644
--- a/src/xrpld/app/consensus/RCLCxTx.h
+++ b/src/xrpld/app/consensus/RCLCxTx.h
@@ -1,6 +1,14 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/consensus/RCLValidations.h b/src/xrpld/app/consensus/RCLValidations.h
index 37a6f6c743..e8a1996204 100644
--- a/src/xrpld/app/consensus/RCLValidations.h
+++ b/src/xrpld/app/consensus/RCLValidations.h
@@ -2,12 +2,23 @@
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/ledger/AcceptedLedger.h b/src/xrpld/app/ledger/AcceptedLedger.h
index b05af1f18a..ec83839d7a 100644
--- a/src/xrpld/app/ledger/AcceptedLedger.h
+++ b/src/xrpld/app/ledger/AcceptedLedger.h
@@ -3,6 +3,11 @@
 #include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/AccountStateSF.h b/src/xrpld/app/ledger/AccountStateSF.h
index 5d00e65120..f5117db4d4 100644
--- a/src/xrpld/app/ledger/AccountStateSF.h
+++ b/src/xrpld/app/ledger/AccountStateSF.h
@@ -2,8 +2,14 @@
 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/BuildLedger.h b/src/xrpld/app/ledger/BuildLedger.h
index 32d45daea3..faa800daa7 100644
--- a/src/xrpld/app/ledger/BuildLedger.h
+++ b/src/xrpld/app/ledger/BuildLedger.h
@@ -3,6 +3,10 @@
 #include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.cpp b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp
index 7c74c10b1b..a8ce70d978 100644
--- a/src/xrpld/app/ledger/ConsensusTransSetSF.cpp
+++ b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp
@@ -6,6 +6,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.h b/src/xrpld/app/ledger/ConsensusTransSetSF.h
index f1d0e26d26..59894add7f 100644
--- a/src/xrpld/app/ledger/ConsensusTransSetSF.h
+++ b/src/xrpld/app/ledger/ConsensusTransSetSF.h
@@ -2,8 +2,15 @@
 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/InboundLedger.h b/src/xrpld/app/ledger/InboundLedger.h
index b82e2f69cd..5f9f0e1baf 100644
--- a/src/xrpld/app/ledger/InboundLedger.h
+++ b/src/xrpld/app/ledger/InboundLedger.h
@@ -2,14 +2,31 @@
 
 #include 
 #include 
+#include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/InboundLedgers.h b/src/xrpld/app/ledger/InboundLedgers.h
index f6c864f777..65b2db7d8e 100644
--- a/src/xrpld/app/ledger/InboundLedgers.h
+++ b/src/xrpld/app/ledger/InboundLedgers.h
@@ -1,9 +1,23 @@
 #pragma once
 
 #include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Manages the lifetime of inbound ledgers.
diff --git a/src/xrpld/app/ledger/InboundTransactions.h b/src/xrpld/app/ledger/InboundTransactions.h
index bd176dbd95..d9799d9ad0 100644
--- a/src/xrpld/app/ledger/InboundTransactions.h
+++ b/src/xrpld/app/ledger/InboundTransactions.h
@@ -2,9 +2,16 @@
 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 
+#include 
+
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/ledger/LedgerCleaner.h b/src/xrpld/app/ledger/LedgerCleaner.h
index 3f76f5593c..fd693d6bec 100644
--- a/src/xrpld/app/ledger/LedgerCleaner.h
+++ b/src/xrpld/app/ledger/LedgerCleaner.h
@@ -6,6 +6,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /** Check the ledger/transaction databases to make sure they have continuity */
diff --git a/src/xrpld/app/ledger/LedgerHistory.h b/src/xrpld/app/ledger/LedgerHistory.h
index 057de7b1bc..3fb6e345cf 100644
--- a/src/xrpld/app/ledger/LedgerHistory.h
+++ b/src/xrpld/app/ledger/LedgerHistory.h
@@ -2,10 +2,17 @@
 
 #include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/ledger/LedgerHolder.h b/src/xrpld/app/ledger/LedgerHolder.h
index 69fe00b439..3e70544bfc 100644
--- a/src/xrpld/app/ledger/LedgerHolder.h
+++ b/src/xrpld/app/ledger/LedgerHolder.h
@@ -2,8 +2,11 @@
 
 #include 
 #include 
+#include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/LedgerMaster.h b/src/xrpld/app/ledger/LedgerMaster.h
index 885ab6db25..efd8c15e20 100644
--- a/src/xrpld/app/ledger/LedgerMaster.h
+++ b/src/xrpld/app/ledger/LedgerMaster.h
@@ -1,25 +1,42 @@
 #pragma once
 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
-#include 
+#include 
 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/LedgerPersistence.h b/src/xrpld/app/ledger/LedgerPersistence.h
index e131932af4..f466c32296 100644
--- a/src/xrpld/app/ledger/LedgerPersistence.h
+++ b/src/xrpld/app/ledger/LedgerPersistence.h
@@ -1,7 +1,11 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/app/ledger/LedgerReplayTask.h b/src/xrpld/app/ledger/LedgerReplayTask.h
index 7bb75253f2..09329761c1 100644
--- a/src/xrpld/app/ledger/LedgerReplayTask.h
+++ b/src/xrpld/app/ledger/LedgerReplayTask.h
@@ -4,6 +4,11 @@
 #include 
 #include 
 
+#include 
+#include 
+
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/ledger/LedgerReplayer.h b/src/xrpld/app/ledger/LedgerReplayer.h
index e2d256f597..d44289121c 100644
--- a/src/xrpld/app/ledger/LedgerReplayer.h
+++ b/src/xrpld/app/ledger/LedgerReplayer.h
@@ -1,11 +1,20 @@
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
+#include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/app/ledger/LedgerToJson.h b/src/xrpld/app/ledger/LedgerToJson.h
index 853d4468cd..c9939fd2f4 100644
--- a/src/xrpld/app/ledger/LedgerToJson.h
+++ b/src/xrpld/app/ledger/LedgerToJson.h
@@ -5,8 +5,12 @@
 #include 
 
 #include 
-#include 
-#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/LocalTxs.h b/src/xrpld/app/ledger/LocalTxs.h
index 1575a18075..fe955eba24 100644
--- a/src/xrpld/app/ledger/LocalTxs.h
+++ b/src/xrpld/app/ledger/LocalTxs.h
@@ -2,7 +2,10 @@
 
 #include 
 #include 
+#include 
+#include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/ledger/OpenLedger.h b/src/xrpld/app/ledger/OpenLedger.h
index 554002d6af..3e0577a9be 100644
--- a/src/xrpld/app/ledger/OpenLedger.h
+++ b/src/xrpld/app/ledger/OpenLedger.h
@@ -3,15 +3,23 @@
 #include 
 
 #include 
-#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.h b/src/xrpld/app/ledger/OrderBookDBImpl.h
index a68f63c043..d57d051cce 100644
--- a/src/xrpld/app/ledger/OrderBookDBImpl.h
+++ b/src/xrpld/app/ledger/OrderBookDBImpl.h
@@ -1,11 +1,22 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/TransactionMaster.h b/src/xrpld/app/ledger/TransactionMaster.h
index 84cbf56e8b..5dcad09f51 100644
--- a/src/xrpld/app/ledger/TransactionMaster.h
+++ b/src/xrpld/app/ledger/TransactionMaster.h
@@ -4,10 +4,19 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 class Application;
diff --git a/src/xrpld/app/ledger/TransactionStateSF.h b/src/xrpld/app/ledger/TransactionStateSF.h
index c5c113be7f..a3f7e7f55a 100644
--- a/src/xrpld/app/ledger/TransactionStateSF.h
+++ b/src/xrpld/app/ledger/TransactionStateSF.h
@@ -2,8 +2,14 @@
 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp
index 4c79068bca..7ac85b892e 100644
--- a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp
@@ -2,6 +2,8 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h
index 57e8fb5652..10c8a21b3a 100644
--- a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h
+++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h
@@ -2,12 +2,22 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
 #include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 class InboundLedgers;
diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp
index 31510b44d2..3fcac03ed5 100644
--- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h
index 260f1ff753..5a8951fb25 100644
--- a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h
+++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h
@@ -1,7 +1,10 @@
 #pragma once
 
 #include 
-#include 
+
+#include 
+
+#include 
 
 namespace xrpl {
 class Application;
diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp
index f9f1aa3188..e7cd031247 100644
--- a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp
index 6c6679ed87..8ebd14083a 100644
--- a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp
+++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp
@@ -1,6 +1,8 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.h b/src/xrpld/app/ledger/detail/SkipListAcquire.h
index 6600b495c9..6f8ceb8b74 100644
--- a/src/xrpld/app/ledger/detail/SkipListAcquire.h
+++ b/src/xrpld/app/ledger/detail/SkipListAcquire.h
@@ -1,11 +1,19 @@
 #pragma once
 
-#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
-#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 class InboundLedgers;
diff --git a/src/xrpld/app/ledger/detail/TimeoutCounter.h b/src/xrpld/app/ledger/detail/TimeoutCounter.h
index 18b443c67d..ab4dd28e47 100644
--- a/src/xrpld/app/ledger/detail/TimeoutCounter.h
+++ b/src/xrpld/app/ledger/detail/TimeoutCounter.h
@@ -2,12 +2,18 @@
 
 #include 
 
+#include 
 #include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.h b/src/xrpld/app/ledger/detail/TransactionAcquire.h
index b94a9f4bfc..5b33066390 100644
--- a/src/xrpld/app/ledger/detail/TransactionAcquire.h
+++ b/src/xrpld/app/ledger/detail/TransactionAcquire.h
@@ -1,9 +1,20 @@
 #pragma once
 
 #include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp
index 08d84636f6..b99c98afa1 100644
--- a/src/xrpld/app/main/Application.cpp
+++ b/src/xrpld/app/main/Application.cpp
@@ -73,6 +73,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/main/Application.h b/src/xrpld/app/main/Application.h
index 08e41e2c4c..33876b97b9 100644
--- a/src/xrpld/app/main/Application.h
+++ b/src/xrpld/app/main/Application.h
@@ -2,17 +2,25 @@
 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
-#include 
 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/main/BasicApp.h b/src/xrpld/app/main/BasicApp.h
index 5ba7c719e3..aa367ca674 100644
--- a/src/xrpld/app/main/BasicApp.h
+++ b/src/xrpld/app/main/BasicApp.h
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/main/CollectorManager.h b/src/xrpld/app/main/CollectorManager.h
index d07da15353..e695ddc956 100644
--- a/src/xrpld/app/main/CollectorManager.h
+++ b/src/xrpld/app/main/CollectorManager.h
@@ -1,8 +1,13 @@
 #pragma once
 
-#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Provides the beast::insight::Collector service. */
diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp
index 1af1343fc5..1146cbdc08 100644
--- a/src/xrpld/app/main/GRPCServer.cpp
+++ b/src/xrpld/app/main/GRPCServer.cpp
@@ -1,5 +1,6 @@
 #include 
 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/main/GRPCServer.h b/src/xrpld/app/main/GRPCServer.h
index 7d299474d9..db948cab99 100644
--- a/src/xrpld/app/main/GRPCServer.h
+++ b/src/xrpld/app/main/GRPCServer.h
@@ -2,16 +2,27 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
-#include 
+#include 
 
 #include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/main/LoadManager.h b/src/xrpld/app/main/LoadManager.h
index 5baa01550e..794048567a 100644
--- a/src/xrpld/app/main/LoadManager.h
+++ b/src/xrpld/app/main/LoadManager.h
@@ -2,7 +2,7 @@
 
 #include 
 
-#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/main/NodeIdentity.h b/src/xrpld/app/main/NodeIdentity.h
index bebd5c261d..789d061021 100644
--- a/src/xrpld/app/main/NodeIdentity.h
+++ b/src/xrpld/app/main/NodeIdentity.h
@@ -7,6 +7,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 /** The cryptographic credentials identifying this server instance.
diff --git a/src/xrpld/app/main/NodeStoreScheduler.h b/src/xrpld/app/main/NodeStoreScheduler.h
index 19e8d9d212..48e606bb45 100644
--- a/src/xrpld/app/main/NodeStoreScheduler.h
+++ b/src/xrpld/app/main/NodeStoreScheduler.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/main/Tuning.h b/src/xrpld/app/main/Tuning.h
index d61f27ec03..0f7a69f5b1 100644
--- a/src/xrpld/app/main/Tuning.h
+++ b/src/xrpld/app/main/Tuning.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/misc/AmendmentTableImpl.h b/src/xrpld/app/misc/AmendmentTableImpl.h
index fe7c067d5a..db71189170 100644
--- a/src/xrpld/app/misc/AmendmentTableImpl.h
+++ b/src/xrpld/app/misc/AmendmentTableImpl.h
@@ -1,8 +1,12 @@
 #pragma once
 
+#include 
 #include 
+#include 
 
-#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/misc/FeeVote.h b/src/xrpld/app/misc/FeeVote.h
index 5c6206558a..d6b4b0fc6a 100644
--- a/src/xrpld/app/misc/FeeVote.h
+++ b/src/xrpld/app/misc/FeeVote.h
@@ -1,9 +1,15 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Manager to process fee votes. */
diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp
index c4e6c3cdc2..363c17f4fa 100644
--- a/src/xrpld/app/misc/FeeVoteImpl.cpp
+++ b/src/xrpld/app/misc/FeeVoteImpl.cpp
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/misc/NegativeUNLVote.h b/src/xrpld/app/misc/NegativeUNLVote.h
index 5c2d3f8630..2896962a84 100644
--- a/src/xrpld/app/misc/NegativeUNLVote.h
+++ b/src/xrpld/app/misc/NegativeUNLVote.h
@@ -1,12 +1,19 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/misc/SHAMapStore.h b/src/xrpld/app/misc/SHAMapStore.h
index 3f43bc8fa4..9f12546463 100644
--- a/src/xrpld/app/misc/SHAMapStore.h
+++ b/src/xrpld/app/misc/SHAMapStore.h
@@ -2,9 +2,14 @@
 
 #include 
 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp
index 33bbf1c613..0389130f7a 100644
--- a/src/xrpld/app/misc/SHAMapStoreImp.cpp
+++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp
@@ -15,6 +15,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h
index 1803c15e71..4025236868 100644
--- a/src/xrpld/app/misc/SHAMapStoreImp.h
+++ b/src/xrpld/app/misc/SHAMapStoreImp.h
@@ -1,17 +1,33 @@
 #pragma once
 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/misc/Transaction.h b/src/xrpld/app/misc/Transaction.h
index 80e041f5b7..ab0fa1f4d8 100644
--- a/src/xrpld/app/misc/Transaction.h
+++ b/src/xrpld/app/misc/Transaction.h
@@ -1,7 +1,11 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -9,8 +13,13 @@
 #include 
 #include 
 #include 
+#include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/misc/TxQ.h b/src/xrpld/app/misc/TxQ.h
index d3caec55cf..135cd592f0 100644
--- a/src/xrpld/app/misc/TxQ.h
+++ b/src/xrpld/app/misc/TxQ.h
@@ -1,17 +1,35 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/misc/ValidatorKeys.h b/src/xrpld/app/misc/ValidatorKeys.h
index 1f96815681..df88ec33a3 100644
--- a/src/xrpld/app/misc/ValidatorKeys.h
+++ b/src/xrpld/app/misc/ValidatorKeys.h
@@ -5,6 +5,8 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h
index 3f1823f930..0db3eee284 100644
--- a/src/xrpld/app/misc/ValidatorList.h
+++ b/src/xrpld/app/misc/ValidatorList.h
@@ -3,17 +3,30 @@
 #include 
 #include 
 
-#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace protocol {
 class TMValidatorList;
diff --git a/src/xrpld/app/misc/ValidatorSite.h b/src/xrpld/app/misc/ValidatorSite.h
index 55f060baf1..7302ebbb52 100644
--- a/src/xrpld/app/misc/ValidatorSite.h
+++ b/src/xrpld/app/misc/ValidatorSite.h
@@ -4,14 +4,21 @@
 #include 
 #include 
 
-#include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/app/misc/detail/AccountTxPaging.h b/src/xrpld/app/misc/detail/AccountTxPaging.h
index a895549322..628ef82c3d 100644
--- a/src/xrpld/app/misc/detail/AccountTxPaging.h
+++ b/src/xrpld/app/misc/detail/AccountTxPaging.h
@@ -1,8 +1,11 @@
 #pragma once
 
+#include 
+#include 
 #include 
 
 #include 
+#include 
 
 //------------------------------------------------------------------------------
 
diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp b/src/xrpld/app/misc/detail/ValidatorSite.cpp
index 6ce3711652..7b51fbd597 100644
--- a/src/xrpld/app/misc/detail/ValidatorSite.cpp
+++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp
@@ -8,7 +8,6 @@
 #include 
 
 #include 
-#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/misc/detail/WorkBase.h b/src/xrpld/app/misc/detail/WorkBase.h
index 56b227613f..5f5fdd28b7 100644
--- a/src/xrpld/app/misc/detail/WorkBase.h
+++ b/src/xrpld/app/misc/detail/WorkBase.h
@@ -2,7 +2,7 @@
 
 #include 
 
-#include 
+#include 
 #include 
 
 #include 
@@ -12,6 +12,8 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl::detail {
diff --git a/src/xrpld/app/misc/detail/WorkFile.h b/src/xrpld/app/misc/detail/WorkFile.h
index 067dc4c38b..2fc7ab952d 100644
--- a/src/xrpld/app/misc/detail/WorkFile.h
+++ b/src/xrpld/app/misc/detail/WorkFile.h
@@ -10,6 +10,9 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl::detail {
diff --git a/src/xrpld/app/misc/detail/WorkPlain.h b/src/xrpld/app/misc/detail/WorkPlain.h
index d3c0309e77..4c45fb4801 100644
--- a/src/xrpld/app/misc/detail/WorkPlain.h
+++ b/src/xrpld/app/misc/detail/WorkPlain.h
@@ -2,6 +2,9 @@
 
 #include 
 
+#include 
+#include 
+
 namespace xrpl::detail {
 
 // Work over TCP/IP
diff --git a/src/xrpld/app/misc/detail/WorkSSL.h b/src/xrpld/app/misc/detail/WorkSSL.h
index 74676bb7c1..d4b3b9ff25 100644
--- a/src/xrpld/app/misc/detail/WorkSSL.h
+++ b/src/xrpld/app/misc/detail/WorkSSL.h
@@ -3,13 +3,14 @@
 #include 
 #include 
 
-#include 
+#include 
 #include 
 
 #include 
 #include 
 
-#include 
+#include 
+#include 
 
 namespace xrpl::detail {
 
diff --git a/src/xrpld/app/misc/make_NetworkOPs.h b/src/xrpld/app/misc/make_NetworkOPs.h
index a848067e36..b4dba2b065 100644
--- a/src/xrpld/app/misc/make_NetworkOPs.h
+++ b/src/xrpld/app/misc/make_NetworkOPs.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
 #include 
@@ -8,6 +8,7 @@
 
 #include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/app/rdb/PeerFinder.h b/src/xrpld/app/rdb/PeerFinder.h
index e5ac6dda8c..4f186ff7e2 100644
--- a/src/xrpld/app/rdb/PeerFinder.h
+++ b/src/xrpld/app/rdb/PeerFinder.h
@@ -1,10 +1,15 @@
 #pragma once
 
-#include 
 #include 
 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /**
diff --git a/src/xrpld/app/rdb/backend/SQLiteDatabase.h b/src/xrpld/app/rdb/backend/SQLiteDatabase.h
index e5c69c2703..f5b930d23e 100644
--- a/src/xrpld/app/rdb/backend/SQLiteDatabase.h
+++ b/src/xrpld/app/rdb/backend/SQLiteDatabase.h
@@ -1,10 +1,22 @@
 #pragma once
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp
index dcc146397b..6e92a0de60 100644
--- a/src/xrpld/app/rdb/backend/detail/Node.cpp
+++ b/src/xrpld/app/rdb/backend/detail/Node.cpp
@@ -43,7 +43,9 @@
 #include   // IWYU pragma: keep
 #include 
 
+#include   // IWYU pragma: keep
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/rdb/backend/detail/Node.h b/src/xrpld/app/rdb/backend/detail/Node.h
index 8267bb1a82..ba3eb91ca3 100644
--- a/src/xrpld/app/rdb/backend/detail/Node.h
+++ b/src/xrpld/app/rdb/backend/detail/Node.h
@@ -2,9 +2,33 @@
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::detail {
 
 /* Need to change TableTypeCount if TableType is modified. */
diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp b/src/xrpld/app/rdb/detail/PeerFinder.cpp
index 9452c3af99..c40e3e42c6 100644
--- a/src/xrpld/app/rdb/detail/PeerFinder.cpp
+++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp
@@ -11,6 +11,7 @@
 
 #include   // IWYU pragma: keep
 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/consensus/Consensus.h b/src/xrpld/consensus/Consensus.h
index b8d04e18b5..fa41f25be1 100644
--- a/src/xrpld/consensus/Consensus.h
+++ b/src/xrpld/consensus/Consensus.h
@@ -3,19 +3,28 @@
 #include 
 #include 
 #include 
-#include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/consensus/ConsensusParms.h b/src/xrpld/consensus/ConsensusParms.h
index e6dd7f046e..d00a5dc3c7 100644
--- a/src/xrpld/consensus/ConsensusParms.h
+++ b/src/xrpld/consensus/ConsensusParms.h
@@ -4,9 +4,9 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/consensus/ConsensusProposal.h b/src/xrpld/consensus/ConsensusProposal.h
index d16679bfb5..c27d5c2819 100644
--- a/src/xrpld/consensus/ConsensusProposal.h
+++ b/src/xrpld/consensus/ConsensusProposal.h
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 /** Represents a proposed position taken during a round of consensus.
diff --git a/src/xrpld/consensus/ConsensusTypes.h b/src/xrpld/consensus/ConsensusTypes.h
index f043fc0663..6b33f50662 100644
--- a/src/xrpld/consensus/ConsensusTypes.h
+++ b/src/xrpld/consensus/ConsensusTypes.h
@@ -3,10 +3,14 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/consensus/DisputedTx.h b/src/xrpld/consensus/DisputedTx.h
index ba8329714b..96df1536f5 100644
--- a/src/xrpld/consensus/DisputedTx.h
+++ b/src/xrpld/consensus/DisputedTx.h
@@ -4,10 +4,15 @@
 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/consensus/LedgerTrie.h b/src/xrpld/consensus/LedgerTrie.h
index b11a69a641..a21eea2a8a 100644
--- a/src/xrpld/consensus/LedgerTrie.h
+++ b/src/xrpld/consensus/LedgerTrie.h
@@ -5,9 +5,13 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/consensus/Validations.h b/src/xrpld/consensus/Validations.h
index d4da8a2887..a204cd0c78 100644
--- a/src/xrpld/consensus/Validations.h
+++ b/src/xrpld/consensus/Validations.h
@@ -5,12 +5,21 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h
index 45b36808b2..285ea7b9ac 100644
--- a/src/xrpld/core/Config.h
+++ b/src/xrpld/core/Config.h
@@ -1,16 +1,20 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include   // VFALCO Breaks levelization
+#include 
 #include 
 
 #include   // VFALCO FIX: This include should not be here
 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/core/TimeKeeper.h b/src/xrpld/core/TimeKeeper.h
index 8eb13e75c0..9e067759ec 100644
--- a/src/xrpld/core/TimeKeeper.h
+++ b/src/xrpld/core/TimeKeeper.h
@@ -4,6 +4,7 @@
 #include 
 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/Cluster.h b/src/xrpld/overlay/Cluster.h
index a8c2083fbc..b3864e0fc6 100644
--- a/src/xrpld/overlay/Cluster.h
+++ b/src/xrpld/overlay/Cluster.h
@@ -7,9 +7,14 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/Compression.h b/src/xrpld/overlay/Compression.h
index fb58bd128a..4b7493e7e6 100644
--- a/src/xrpld/overlay/Compression.h
+++ b/src/xrpld/overlay/Compression.h
@@ -2,6 +2,10 @@
 
 #include 
 #include 
+#include 
+
+#include 
+#include 
 
 namespace xrpl::compression {
 
diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h
index cd21ca40c6..63aa360f92 100644
--- a/src/xrpld/overlay/Message.h
+++ b/src/xrpld/overlay/Message.h
@@ -4,10 +4,17 @@
 
 #include 
 #include 
-#include 
 
-#include 
+#include 
+
+#include 
+
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/Overlay.h b/src/xrpld/overlay/Overlay.h
index 87c6ff132a..cc5be791a7 100644
--- a/src/xrpld/overlay/Overlay.h
+++ b/src/xrpld/overlay/Overlay.h
@@ -2,8 +2,12 @@
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -11,8 +15,15 @@
 #include 
 #include 
 
+#include 
+
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace boost::asio::ssl {
 class context;
diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h
index 0c86776030..29778b42a6 100644
--- a/src/xrpld/overlay/Peer.h
+++ b/src/xrpld/overlay/Peer.h
@@ -7,6 +7,12 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 namespace Resource {
diff --git a/src/xrpld/overlay/PeerSet.h b/src/xrpld/overlay/PeerSet.h
index f50b7130f4..4670ec9783 100644
--- a/src/xrpld/overlay/PeerSet.h
+++ b/src/xrpld/overlay/PeerSet.h
@@ -4,6 +4,15 @@
 #include 
 #include 
 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Supports data retrieval by managing a set of peers.
diff --git a/src/xrpld/overlay/ReduceRelayCommon.h b/src/xrpld/overlay/ReduceRelayCommon.h
index 2389c21f0e..243ce167f9 100644
--- a/src/xrpld/overlay/ReduceRelayCommon.h
+++ b/src/xrpld/overlay/ReduceRelayCommon.h
@@ -1,6 +1,8 @@
 #pragma once
 
 #include 
+#include 
+#include 
 
 // Blog post explaining the rationale behind reduction of flooding gossip
 // protocol:
diff --git a/src/xrpld/overlay/Slot.h b/src/xrpld/overlay/Slot.h
index 7490798787..a29020e03a 100644
--- a/src/xrpld/overlay/Slot.h
+++ b/src/xrpld/overlay/Slot.h
@@ -5,19 +5,33 @@
 #include 
 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
-#include 
+
+#include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl::reduce_relay {
 
diff --git a/src/xrpld/overlay/Squelch.h b/src/xrpld/overlay/Squelch.h
index 96d8c26f1d..0485c91c81 100644
--- a/src/xrpld/overlay/Squelch.h
+++ b/src/xrpld/overlay/Squelch.h
@@ -3,6 +3,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h
index aba224d5c7..bed3f672be 100644
--- a/src/xrpld/overlay/detail/ConnectAttempt.h
+++ b/src/xrpld/overlay/detail/ConnectAttempt.h
@@ -1,9 +1,21 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/detail/Handshake.h b/src/xrpld/overlay/detail/Handshake.h
index 3cbaa118da..6dcc06bbf1 100644
--- a/src/xrpld/overlay/detail/Handshake.h
+++ b/src/xrpld/overlay/detail/Handshake.h
@@ -3,8 +3,9 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
-#include 
 
 #include 
 #include 
@@ -13,8 +14,9 @@
 #include 
 #include 
 
+#include 
 #include 
-#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h
index 545d9eb75c..7db32c0d23 100644
--- a/src/xrpld/overlay/detail/OverlayImpl.h
+++ b/src/xrpld/overlay/detail/OverlayImpl.h
@@ -3,20 +3,30 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -25,14 +35,22 @@
 #include 
 #include 
 
+#include 
+
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h
index 28fb6b33a4..4a96f9a6e6 100644
--- a/src/xrpld/overlay/detail/PeerImp.h
+++ b/src/xrpld/overlay/detail/PeerImp.h
@@ -2,28 +2,59 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 #include 
 #include 
 #include 
 
+#include 
+
+#include 
+
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h
index b1a30bad10..da418f8e82 100644
--- a/src/xrpld/overlay/detail/ProtocolMessage.h
+++ b/src/xrpld/overlay/detail/ProtocolMessage.h
@@ -5,14 +5,21 @@
 #include 
 
 #include 
-#include 
 
 #include 
 #include 
 
+#include 
+
+#include 
+
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/overlay/detail/TrafficCount.h b/src/xrpld/overlay/detail/TrafficCount.h
index cb77c2359f..b96ee022d6 100644
--- a/src/xrpld/overlay/detail/TrafficCount.h
+++ b/src/xrpld/overlay/detail/TrafficCount.h
@@ -1,10 +1,16 @@
 #pragma once
 
 #include 
-#include 
+
+#include 
+
+#include 
 
 #include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/overlay/detail/TxMetrics.h b/src/xrpld/overlay/detail/TxMetrics.h
index 50df7a65cc..44cd0272ee 100644
--- a/src/xrpld/overlay/detail/TxMetrics.h
+++ b/src/xrpld/overlay/detail/TxMetrics.h
@@ -1,11 +1,13 @@
 #pragma once
 
 #include 
-#include 
 
 #include 
 
+#include 
+
 #include 
+#include 
 #include 
 
 namespace xrpl::metrics {
diff --git a/src/xrpld/overlay/detail/ZeroCopyStream.h b/src/xrpld/overlay/detail/ZeroCopyStream.h
index f77f266321..deccb627b9 100644
--- a/src/xrpld/overlay/detail/ZeroCopyStream.h
+++ b/src/xrpld/overlay/detail/ZeroCopyStream.h
@@ -6,6 +6,9 @@
 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Implements ZeroCopyInputStream around a buffer sequence.
@@ -20,7 +23,7 @@ private:
     using iterator = Buffers::const_iterator;
     using const_buffer = boost::asio::const_buffer;
 
-    google::protobuf::int64 count_ = 0;
+    std::int64_t count_ = 0;
     iterator last_;
     iterator first_;    // Where pos_ comes from
     const_buffer pos_;  // What Next() will return
@@ -37,7 +40,7 @@ public:
     bool
     Skip(int count) override;
 
-    [[nodiscard]] google::protobuf::int64
+    [[nodiscard]] std::int64_t
     ByteCount() const override
     {
         return count_;
@@ -116,7 +119,7 @@ private:
 
     Streambuf& streambuf_;
     std::size_t blockSize_;
-    google::protobuf::int64 count_ = 0;
+    std::int64_t count_ = 0;
     std::size_t commit_ = 0;
     buffers_type buffers_;
     iterator pos_;
@@ -132,7 +135,7 @@ public:
     void
     BackUp(int count) override;
 
-    [[nodiscard]] google::protobuf::int64
+    [[nodiscard]] std::int64_t
     ByteCount() const override
     {
         return count_;
diff --git a/src/xrpld/overlay/make_Overlay.h b/src/xrpld/overlay/make_Overlay.h
index acd477deec..f0dfa429c0 100644
--- a/src/xrpld/overlay/make_Overlay.h
+++ b/src/xrpld/overlay/make_Overlay.h
@@ -1,12 +1,19 @@
 #pragma once
 
+#include 
 #include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 Overlay::Setup
diff --git a/src/xrpld/overlay/predicates.h b/src/xrpld/overlay/predicates.h
index d7c2add937..2527b8c728 100644
--- a/src/xrpld/overlay/predicates.h
+++ b/src/xrpld/overlay/predicates.h
@@ -3,6 +3,7 @@
 #include 
 #include 
 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h
index ec4beb2db4..d482ae7241 100644
--- a/src/xrpld/peerfinder/PeerfinderManager.h
+++ b/src/xrpld/peerfinder/PeerfinderManager.h
@@ -5,11 +5,20 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/Slot.h b/src/xrpld/peerfinder/Slot.h
index 289252e3fa..f43b7d1009 100644
--- a/src/xrpld/peerfinder/Slot.h
+++ b/src/xrpld/peerfinder/Slot.h
@@ -3,6 +3,8 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 
 namespace xrpl::PeerFinder {
diff --git a/src/xrpld/peerfinder/detail/Bootcache.h b/src/xrpld/peerfinder/detail/Bootcache.h
index 01ee2cad33..5384bb356a 100644
--- a/src/xrpld/peerfinder/detail/Bootcache.h
+++ b/src/xrpld/peerfinder/detail/Bootcache.h
@@ -4,6 +4,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/peerfinder/detail/Checker.h b/src/xrpld/peerfinder/detail/Checker.h
index 2f324bf8b6..20687448df 100644
--- a/src/xrpld/peerfinder/detail/Checker.h
+++ b/src/xrpld/peerfinder/detail/Checker.h
@@ -1,12 +1,14 @@
 #pragma once
 
 #include 
+#include 
 
 #include 
 #include 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/peerfinder/detail/Counts.h b/src/xrpld/peerfinder/detail/Counts.h
index 67b8370996..0d8bf1c56e 100644
--- a/src/xrpld/peerfinder/detail/Counts.h
+++ b/src/xrpld/peerfinder/detail/Counts.h
@@ -4,7 +4,12 @@
 #include 
 #include 
 
-#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/detail/Fixed.h b/src/xrpld/peerfinder/detail/Fixed.h
index 61df9caddb..3319994251 100644
--- a/src/xrpld/peerfinder/detail/Fixed.h
+++ b/src/xrpld/peerfinder/detail/Fixed.h
@@ -1,7 +1,12 @@
 #pragma once
 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl::PeerFinder {
 
 /** Metadata for a Fixed slot. */
diff --git a/src/xrpld/peerfinder/detail/Handouts.h b/src/xrpld/peerfinder/detail/Handouts.h
index 7523197c40..faadb51fd2 100644
--- a/src/xrpld/peerfinder/detail/Handouts.h
+++ b/src/xrpld/peerfinder/detail/Handouts.h
@@ -1,12 +1,17 @@
 #pragma once
 
+#include 
 #include 
 #include 
 
 #include 
+#include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/detail/Livecache.h b/src/xrpld/peerfinder/detail/Livecache.h
index c5f04be90a..1dcc8f6daf 100644
--- a/src/xrpld/peerfinder/detail/Livecache.h
+++ b/src/xrpld/peerfinder/detail/Livecache.h
@@ -7,13 +7,27 @@
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 #include 
 #include 
 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h
index b74643f7a5..7d10f7f516 100644
--- a/src/xrpld/peerfinder/detail/Logic.h
+++ b/src/xrpld/peerfinder/detail/Logic.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -14,14 +15,29 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/detail/SlotImp.h b/src/xrpld/peerfinder/detail/SlotImp.h
index 0cf4d7a3c8..cf1915268f 100644
--- a/src/xrpld/peerfinder/detail/SlotImp.h
+++ b/src/xrpld/peerfinder/detail/SlotImp.h
@@ -4,9 +4,14 @@
 #include 
 
 #include 
+#include 
+#include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/detail/Source.h b/src/xrpld/peerfinder/detail/Source.h
index c86176911e..cf8920e056 100644
--- a/src/xrpld/peerfinder/detail/Source.h
+++ b/src/xrpld/peerfinder/detail/Source.h
@@ -2,8 +2,12 @@
 
 #include 
 
+#include 
+
 #include 
 
+#include 
+
 namespace xrpl::PeerFinder {
 
 /** A static or dynamic source of peer addresses.
diff --git a/src/xrpld/peerfinder/detail/SourceStrings.h b/src/xrpld/peerfinder/detail/SourceStrings.h
index 156db0ce85..618970fa03 100644
--- a/src/xrpld/peerfinder/detail/SourceStrings.h
+++ b/src/xrpld/peerfinder/detail/SourceStrings.h
@@ -3,6 +3,8 @@
 #include 
 
 #include 
+#include 
+#include 
 
 namespace xrpl::PeerFinder {
 
diff --git a/src/xrpld/peerfinder/detail/Store.h b/src/xrpld/peerfinder/detail/Store.h
index 347fc09b15..570dba0523 100644
--- a/src/xrpld/peerfinder/detail/Store.h
+++ b/src/xrpld/peerfinder/detail/Store.h
@@ -1,5 +1,11 @@
 #pragma once
 
+#include 
+
+#include 
+#include 
+#include 
+
 namespace xrpl::PeerFinder {
 
 /** Abstract persistence for PeerFinder data. */
diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h b/src/xrpld/peerfinder/detail/StoreSqdb.h
index 19cdd774b7..f868e89ff7 100644
--- a/src/xrpld/peerfinder/detail/StoreSqdb.h
+++ b/src/xrpld/peerfinder/detail/StoreSqdb.h
@@ -3,8 +3,17 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+
+#include 
+#include 
+#include 
+
 namespace xrpl::PeerFinder {
 
 /** Database persistence for PeerFinder using SQLite */
diff --git a/src/xrpld/peerfinder/detail/Tuning.h b/src/xrpld/peerfinder/detail/Tuning.h
index b4781495b8..1bf9df382e 100644
--- a/src/xrpld/peerfinder/detail/Tuning.h
+++ b/src/xrpld/peerfinder/detail/Tuning.h
@@ -1,6 +1,9 @@
 #pragma once
 
+#include 
 #include 
+#include 
+#include 
 
 /** Heuristically tuned constants. */
 /** @{ */
diff --git a/src/xrpld/peerfinder/detail/iosformat.h b/src/xrpld/peerfinder/detail/iosformat.h
index 632ac10c16..a0b9ff537a 100644
--- a/src/xrpld/peerfinder/detail/iosformat.h
+++ b/src/xrpld/peerfinder/detail/iosformat.h
@@ -1,5 +1,8 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/peerfinder/make_Manager.h b/src/xrpld/peerfinder/make_Manager.h
index 846330988b..1f3f226397 100644
--- a/src/xrpld/peerfinder/make_Manager.h
+++ b/src/xrpld/peerfinder/make_Manager.h
@@ -2,6 +2,10 @@
 
 #include 
 
+#include 
+#include 
+#include 
+
 #include 
 
 #include 
diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp
index 5ace4d8c8b..3aa7e38ea2 100644
--- a/src/xrpld/perflog/detail/PerfLogImp.cpp
+++ b/src/xrpld/perflog/detail/PerfLogImp.cpp
@@ -1,5 +1,7 @@
 #include 
 
+#include 
+
 #include 
 #include 
 #include 
@@ -12,6 +14,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
diff --git a/src/xrpld/perflog/detail/PerfLogImp.h b/src/xrpld/perflog/detail/PerfLogImp.h
index bd60912c18..88bf473554 100644
--- a/src/xrpld/perflog/detail/PerfLogImp.h
+++ b/src/xrpld/perflog/detail/PerfLogImp.h
@@ -3,17 +3,23 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 #include 
 
 #include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl::perf {
diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h
index 45c3cf2e4b..3c10ece78f 100644
--- a/src/xrpld/rpc/BookChanges.h
+++ b/src/xrpld/rpc/BookChanges.h
@@ -1,13 +1,25 @@
 #pragma once
 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 namespace json {
 class Value;
diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h
index 781cd85e29..c2133a19b5 100644
--- a/src/xrpld/rpc/CTID.h
+++ b/src/xrpld/rpc/CTID.h
@@ -2,9 +2,14 @@
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
-#include 
 #include 
+#include 
+#include 
+#include 
 
 namespace xrpl::RPC {
 
diff --git a/src/xrpld/rpc/Context.h b/src/xrpld/rpc/Context.h
index 3724ce1783..fe6bb81ce3 100644
--- a/src/xrpld/rpc/Context.h
+++ b/src/xrpld/rpc/Context.h
@@ -4,8 +4,14 @@
 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 class Application;
diff --git a/src/xrpld/rpc/DeliveredAmount.h b/src/xrpld/rpc/DeliveredAmount.h
index b603fa4acd..bba045d494 100644
--- a/src/xrpld/rpc/DeliveredAmount.h
+++ b/src/xrpld/rpc/DeliveredAmount.h
@@ -3,8 +3,8 @@
 #include 
 #include 
 
-#include 
 #include 
+#include 
 
 namespace json {
 class Value;
diff --git a/src/xrpld/rpc/GRPCHandlers.h b/src/xrpld/rpc/GRPCHandlers.h
index ac419c18ee..9dc7e0b13a 100644
--- a/src/xrpld/rpc/GRPCHandlers.h
+++ b/src/xrpld/rpc/GRPCHandlers.h
@@ -2,9 +2,13 @@
 
 #include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
-#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h
index 8a8f6f3d51..cb2bfd1bdc 100644
--- a/src/xrpld/rpc/MPTokenIssuanceID.h
+++ b/src/xrpld/rpc/MPTokenIssuanceID.h
@@ -1,9 +1,9 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
diff --git a/src/xrpld/rpc/Output.h b/src/xrpld/rpc/Output.h
index 1c74562842..30b5c090d7 100644
--- a/src/xrpld/rpc/Output.h
+++ b/src/xrpld/rpc/Output.h
@@ -2,8 +2,10 @@
 
 #include 
 
-namespace xrpl {
-namespace RPC {
+#include 
+#include 
+
+namespace xrpl::RPC {
 
 using Output = std::function;
 
@@ -13,5 +15,4 @@ stringOutput(std::string& s)
     return [&](boost::string_ref const& b) { s.append(b.data(), b.size()); };
 }
 
-}  // namespace RPC
-}  // namespace xrpl
+}  // namespace xrpl::RPC
diff --git a/src/xrpld/rpc/RPCCall.h b/src/xrpld/rpc/RPCCall.h
index 7a09115e42..a06eca4413 100644
--- a/src/xrpld/rpc/RPCCall.h
+++ b/src/xrpld/rpc/RPCCall.h
@@ -2,14 +2,17 @@
 
 #include 
 
-#include 
+#include 
+#include 
 #include 
 
 #include 
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/rpc/RPCHandler.h b/src/xrpld/rpc/RPCHandler.h
index 7e57b456fc..d1cd54145d 100644
--- a/src/xrpld/rpc/RPCHandler.h
+++ b/src/xrpld/rpc/RPCHandler.h
@@ -1,8 +1,13 @@
 #pragma once
 
 #include 
+#include 
 #include 
 
+#include 
+
+#include 
+
 namespace xrpl::RPC {
 
 struct JsonContext;
diff --git a/src/xrpld/rpc/RPCSub.h b/src/xrpld/rpc/RPCSub.h
index 4c0903ed3e..95206e5cf6 100644
--- a/src/xrpld/rpc/RPCSub.h
+++ b/src/xrpld/rpc/RPCSub.h
@@ -6,6 +6,9 @@
 
 #include 
 
+#include 
+#include 
+
 namespace xrpl {
 
 /** Subscription object for JSON RPC. */
diff --git a/src/xrpld/rpc/Role.h b/src/xrpld/rpc/Role.h
index d1b641f067..660fb92c7c 100644
--- a/src/xrpld/rpc/Role.h
+++ b/src/xrpld/rpc/Role.h
@@ -1,7 +1,9 @@
 #pragma once
 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -10,7 +12,7 @@
 #include 
 #include 
 
-#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/rpc/ServerHandler.h b/src/xrpld/rpc/ServerHandler.h
index 0aac05ea44..054bec9b5b 100644
--- a/src/xrpld/rpc/ServerHandler.h
+++ b/src/xrpld/rpc/ServerHandler.h
@@ -2,11 +2,19 @@
 
 #include 
 #include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 
@@ -15,8 +23,15 @@
 #include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl {
diff --git a/src/xrpld/rpc/Status.h b/src/xrpld/rpc/Status.h
index 8f7c620baa..4f15ab0242 100644
--- a/src/xrpld/rpc/Status.h
+++ b/src/xrpld/rpc/Status.h
@@ -1,9 +1,16 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::RPC {
 
 /** Status represents the results of an operation that might fail.
diff --git a/src/xrpld/rpc/detail/AccountAssets.h b/src/xrpld/rpc/detail/AccountAssets.h
index 55fce62fa6..16c6087e3f 100644
--- a/src/xrpld/rpc/detail/AccountAssets.h
+++ b/src/xrpld/rpc/detail/AccountAssets.h
@@ -2,7 +2,11 @@
 
 #include 
 
-#include 
+#include 
+#include 
+#include 
+
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/AssetCache.h b/src/xrpld/rpc/detail/AssetCache.h
index e53bc3ff94..4b89487526 100644
--- a/src/xrpld/rpc/detail/AssetCache.h
+++ b/src/xrpld/rpc/detail/AssetCache.h
@@ -4,10 +4,14 @@
 #include 
 
 #include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 
 #include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/rpc/detail/Handler.h b/src/xrpld/rpc/detail/Handler.h
index 140f421ed3..5e583aa5bb 100644
--- a/src/xrpld/rpc/detail/Handler.h
+++ b/src/xrpld/rpc/detail/Handler.h
@@ -1,13 +1,21 @@
 #pragma once
 
-#include 
 #include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace json {
 class Object;
 }  // namespace json
diff --git a/src/xrpld/rpc/detail/MPT.h b/src/xrpld/rpc/detail/MPT.h
index 5cf2d5490f..93c8517539 100644
--- a/src/xrpld/rpc/detail/MPT.h
+++ b/src/xrpld/rpc/detail/MPT.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/PathRequest.h b/src/xrpld/rpc/detail/PathRequest.h
index 372223e99f..64bc6ef181 100644
--- a/src/xrpld/rpc/detail/PathRequest.h
+++ b/src/xrpld/rpc/detail/PathRequest.h
@@ -1,19 +1,31 @@
 #pragma once
 
+#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 #include 
-#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/PathRequestManager.h b/src/xrpld/rpc/detail/PathRequestManager.h
index c8e272a97d..94d126ed23 100644
--- a/src/xrpld/rpc/detail/PathRequestManager.h
+++ b/src/xrpld/rpc/detail/PathRequestManager.h
@@ -4,7 +4,17 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/rpc/detail/Pathfinder.h b/src/xrpld/rpc/detail/Pathfinder.h
index 36caab308e..5ef6c31b25 100644
--- a/src/xrpld/rpc/detail/Pathfinder.h
+++ b/src/xrpld/rpc/detail/Pathfinder.h
@@ -4,11 +4,25 @@
 #include 
 
 #include 
+#include 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/PathfinderUtils.h b/src/xrpld/rpc/detail/PathfinderUtils.h
index a703127148..ad48b3e9d7 100644
--- a/src/xrpld/rpc/detail/PathfinderUtils.h
+++ b/src/xrpld/rpc/detail/PathfinderUtils.h
@@ -1,6 +1,10 @@
 #pragma once
 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp
index 7e30b81fa6..e839d9d78d 100644
--- a/src/xrpld/rpc/detail/RPCHandler.cpp
+++ b/src/xrpld/rpc/detail/RPCHandler.cpp
@@ -1,5 +1,6 @@
 #include 
 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h
index bbc101a072..4a4dca42e5 100644
--- a/src/xrpld/rpc/detail/RPCHelpers.h
+++ b/src/xrpld/rpc/detail/RPCHelpers.h
@@ -1,16 +1,27 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
 #include 
+#include 
 #include 
 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp
index 38827dc93c..6843c34b19 100644
--- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp
+++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp
@@ -1,6 +1,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -20,6 +21,9 @@
 #include 
 #include 
 
+#include         // IWYU pragma: keep
+#include    // IWYU pragma: keep
+#include   // IWYU pragma: keep
 #include 
 
 #include 
diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h b/src/xrpld/rpc/detail/RPCLedgerHelpers.h
index 9ec2a6673c..cbd47d38e6 100644
--- a/src/xrpld/rpc/detail/RPCLedgerHelpers.h
+++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h
@@ -1,17 +1,20 @@
 #pragma once
 
-#include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
-#include 
 #include 
 #include 
 
+#include 
+
+#include 
 #include 
-#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/TransactionSign.h b/src/xrpld/rpc/detail/TransactionSign.h
index 1fd9e87b54..cb3fb176dc 100644
--- a/src/xrpld/rpc/detail/TransactionSign.h
+++ b/src/xrpld/rpc/detail/TransactionSign.h
@@ -4,9 +4,14 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 // Forward declarations
diff --git a/src/xrpld/rpc/detail/TrustLine.h b/src/xrpld/rpc/detail/TrustLine.h
index 7a0a01d744..a80d81e4ae 100644
--- a/src/xrpld/rpc/detail/TrustLine.h
+++ b/src/xrpld/rpc/detail/TrustLine.h
@@ -1,13 +1,18 @@
 #pragma once
 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h
index 12c09bcb15..5a9d546472 100644
--- a/src/xrpld/rpc/detail/Tuning.h
+++ b/src/xrpld/rpc/detail/Tuning.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include 
+
 /** Tuned constants. */
 /** @{ */
 namespace xrpl::RPC::Tuning {
diff --git a/src/xrpld/rpc/detail/WSInfoSub.h b/src/xrpld/rpc/detail/WSInfoSub.h
index c224b93e0e..cefb7ad2e4 100644
--- a/src/xrpld/rpc/detail/WSInfoSub.h
+++ b/src/xrpld/rpc/detail/WSInfoSub.h
@@ -7,8 +7,11 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h
index 2d39e42e02..7b347b2ecc 100644
--- a/src/xrpld/rpc/handlers/Handlers.h
+++ b/src/xrpld/rpc/handlers/Handlers.h
@@ -2,6 +2,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 json::Value
diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp
index 0249963908..5ce10f6121 100644
--- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp
@@ -12,6 +12,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp
index 40042a0390..c2a8319876 100644
--- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp
+++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp
@@ -1,6 +1,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp
index 95da4c8567..e0204159fd 100644
--- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp
+++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp
@@ -1,6 +1,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.h b/src/xrpld/rpc/handlers/admin/status/GetCounts.h
index 05751637ee..15cfa32a29 100644
--- a/src/xrpld/rpc/handlers/admin/status/GetCounts.h
+++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.h
@@ -2,6 +2,8 @@
 
 #include 
 
+#include 
+
 namespace xrpl {
 
 json::Value
diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h b/src/xrpld/rpc/handlers/ledger/Ledger.h
index 719e635170..59b64832f7 100644
--- a/src/xrpld/rpc/handlers/ledger/Ledger.h
+++ b/src/xrpld/rpc/handlers/ledger/Ledger.h
@@ -1,16 +1,18 @@
 #pragma once
 
-#include 
-#include 
 #include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
-#include 
+
+#include 
+#include 
 
 namespace json {
 class Object;
diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h
index 11f6553dfa..57c4e58242 100644
--- a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h
+++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h
@@ -2,18 +2,25 @@
 
 #include 
 
+#include 
 #include 
-#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
 #include 
-#include 
-#include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 
 namespace xrpl::LedgerEntryHelpers {
 
diff --git a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp
index 947a257916..abad196246 100644
--- a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp
+++ b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp
@@ -4,6 +4,7 @@
 #include 
 
 #include 
+#include   // IWYU pragma: keep
 
 #include 
 
diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp
index 6a75277b1b..c1c8712655 100644
--- a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp
+++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp
@@ -17,6 +17,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
index 42b99a0009..70bc258d77 100644
--- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
+++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
@@ -5,15 +5,25 @@
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
-#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 inline void
diff --git a/src/xrpld/rpc/handlers/server_info/Version.h b/src/xrpld/rpc/handlers/server_info/Version.h
index 1868df5d40..f25d8679ba 100644
--- a/src/xrpld/rpc/handlers/server_info/Version.h
+++ b/src/xrpld/rpc/handlers/server_info/Version.h
@@ -1,5 +1,12 @@
 #pragma once
 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+#include 
+
+#include 
 #include 
 
 namespace xrpl::RPC {
diff --git a/src/xrpld/rpc/json_body.h b/src/xrpld/rpc/json_body.h
index 5c4c56cef8..49c2b0e6e0 100644
--- a/src/xrpld/rpc/json_body.h
+++ b/src/xrpld/rpc/json_body.h
@@ -6,6 +6,11 @@
 #include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl {
 
 /// Body that holds JSON
diff --git a/src/xrpld/shamap/NodeFamily.cpp b/src/xrpld/shamap/NodeFamily.cpp
index a1668e80b2..2e48117d6a 100644
--- a/src/xrpld/shamap/NodeFamily.cpp
+++ b/src/xrpld/shamap/NodeFamily.cpp
@@ -1,6 +1,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
diff --git a/src/xrpld/shamap/NodeFamily.h b/src/xrpld/shamap/NodeFamily.h
index e0655292a0..d532f13ecc 100644
--- a/src/xrpld/shamap/NodeFamily.h
+++ b/src/xrpld/shamap/NodeFamily.h
@@ -2,8 +2,17 @@
 
 #include 
 
+#include 
+#include 
+#include 
 #include 
 #include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
 
 namespace xrpl {
 

From 4c619e8a85cc1ca162a9d12522548d51c29c8170 Mon Sep 17 00:00:00 2001
From: Timothy Banks 
Date: Thu, 2 Jul 2026 10:17:32 -0400
Subject: [PATCH 037/100] refactor: Retire DisallowIncomingV1 fix (#7364)

---
 include/xrpl/protocol/detail/features.macro   |  2 +-
 src/libxrpl/tx/transactors/token/TrustSet.cpp | 18 +----
 src/test/app/TrustSet_test.cpp                | 69 +++++++++----------
 3 files changed, 37 insertions(+), 52 deletions(-)

diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index 7e64a49b0c..a7a62f0322 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -60,7 +60,6 @@ XRPL_FEATURE(PriceOracle,                Supported::Yes, VoteBehavior::DefaultNo
 XRPL_FIX    (AMMOverflowOffer,           Supported::Yes, VoteBehavior::DefaultYes)
 XRPL_FIX    (FillOrKill,                 Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(DID,                        Supported::Yes, VoteBehavior::DefaultNo)
-XRPL_FIX    (DisallowIncomingV1,         Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(XChainBridge,               Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(AMM,                        Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(XRPFees,                    Supported::Yes, VoteBehavior::DefaultNo)
@@ -100,6 +99,7 @@ XRPL_RETIRE_FIX(1623)
 XRPL_RETIRE_FIX(1781)
 XRPL_RETIRE_FIX(AmendmentMajorityCalc)
 XRPL_RETIRE_FIX(CheckThreading)
+XRPL_RETIRE_FIX(DisallowIncomingV1)
 XRPL_RETIRE_FIX(InnerObjTemplate)
 XRPL_RETIRE_FIX(MasterKeyAsRegularKey)
 XRPL_RETIRE_FIX(NonFungibleTokensV1_2)
diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp
index 151e82bab9..e79e3cad5a 100644
--- a/src/libxrpl/tx/transactors/token/TrustSet.cpp
+++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp
@@ -184,22 +184,10 @@ TrustSet::preclaim(PreclaimContext const& ctx)
 
     // If the destination has opted to disallow incoming trustlines
     // then honour that flag
-    if (sleDst->isFlag(lsfDisallowIncomingTrustline))
+    if (sleDst && sleDst->isFlag(lsfDisallowIncomingTrustline) &&
+        !ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency)))
     {
-        // The original implementation of featureDisallowIncoming was
-        // too restrictive. If
-        //   o fixDisallowIncomingV1 is enabled and
-        //   o The trust line already exists
-        // Then allow the TrustSet.
-        if (ctx.view.rules().enabled(fixDisallowIncomingV1) &&
-            ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency)))
-        {
-            // pass
-        }
-        else
-        {
-            return tecNO_PERMISSION;
-        }
+        return tecNO_PERMISSION;
     }
 
     // In general, trust lines to pseudo accounts are not permitted, unless
diff --git a/src/test/app/TrustSet_test.cpp b/src/test/app/TrustSet_test.cpp
index e9f0553840..15abc3bd06 100644
--- a/src/test/app/TrustSet_test.cpp
+++ b/src/test/app/TrustSet_test.cpp
@@ -474,6 +474,38 @@ public:
         checkQuality(!createQuality);
     }
 
+    void
+    testDisallowIncomingWithRequireAuth()
+    {
+        testcase("Create trustline with disallow incoming requiring auth");
+
+        using namespace test::jtx;
+
+        Env env{*this};
+        auto const dist = Account("dist");
+        auto const gw = Account("gw");
+        auto const usd = gw["USD"];
+        auto const distUSD = dist["USD"];
+
+        env.fund(XRP(1000), gw, dist);
+        env.close();
+
+        env(fset(gw, asfRequireAuth));
+        env.close();
+
+        env(fset(dist, asfDisallowIncomingTrustline));
+        env.close();
+
+        env(trust(dist, usd(10000)));
+        env.close();
+
+        env(trust(gw, distUSD(10000)), Txflags(tfSetfAuth), Ter(tesSUCCESS));
+        env.close();
+
+        env(pay(gw, dist, usd(1000)), Ter(tesSUCCESS));
+        env.close();
+    }
+
     void
     testDisallowIncoming(FeatureBitset features)
     {
@@ -481,42 +513,6 @@ public:
 
         using namespace test::jtx;
 
-        // fixDisallowIncomingV1
-        {
-            for (bool const withFix : {true, false})
-            {
-                auto const amend = withFix ? features : features - fixDisallowIncomingV1;
-
-                Env env{*this, amend};
-                auto const dist = Account("dist");
-                auto const gw = Account("gw");
-                auto const usd = gw["USD"];
-                auto const distUSD = dist["USD"];
-
-                env.fund(XRP(1000), gw, dist);
-                env.close();
-
-                env(fset(gw, asfRequireAuth));
-                env.close();
-
-                env(fset(dist, asfDisallowIncomingTrustline));
-                env.close();
-
-                env(trust(dist, usd(10000)));
-                env.close();
-
-                // withFix: can set trustline
-                // withOutFix: cannot set trustline
-                auto const trustResult = withFix ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION);
-                env(trust(gw, distUSD(10000)), Txflags(tfSetfAuth), trustResult);
-                env.close();
-
-                auto const txResult = withFix ? Ter(tesSUCCESS) : Ter(tecPATH_DRY);
-                env(pay(gw, dist, usd(1000)), txResult);
-                env.close();
-            }
-        }
-
         Env env{*this, features};
 
         auto const gw = Account{"gateway"};
@@ -601,6 +597,7 @@ public:
         testModifyQualityOfTrustline(features, false, true);
         testModifyQualityOfTrustline(features, true, false);
         testModifyQualityOfTrustline(features, true, true);
+        testDisallowIncomingWithRequireAuth();
         testDisallowIncoming(features);
         testTrustLineResetWithAuthFlag();
         testTrustLineDelete();

From 3b9e24e0e0bf3a5941808446ac1fca28bd8e8e69 Mon Sep 17 00:00:00 2001
From: Bart 
Date: Thu, 2 Jul 2026 11:01:30 -0400
Subject: [PATCH 038/100] chore: Improve pre-commit hooks (#7702)

Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
---
 cspell.config.yaml => .cspell.config.yaml | 4 +---
 .pre-commit-config.yaml                   | 9 ++++-----
 2 files changed, 5 insertions(+), 8 deletions(-)
 rename cspell.config.yaml => .cspell.config.yaml (96%)

diff --git a/cspell.config.yaml b/.cspell.config.yaml
similarity index 96%
rename from cspell.config.yaml
rename to .cspell.config.yaml
index c120c31855..c558fc0984 100644
--- a/cspell.config.yaml
+++ b/.cspell.config.yaml
@@ -36,9 +36,7 @@ overrides:
       - /'[^']*'/g # single-quoted strings
       - /`[^`]*`/g # backtick strings
 suggestWords:
-  - xprl->xrpl
-  - xprld->xrpld # cspell: disable-line not sure what this problem is....
-  - unsynched->unsynced # cspell: disable-line not sure what this problem is....
+  - unsynched->unsynced
   - synched->synced
   - synch->sync
 words:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2e4521870d..910bda8d4c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -90,20 +90,19 @@ repos:
   - repo: https://github.com/streetsidesoftware/cspell-cli
     rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1
     hooks:
-      - id: cspell # Spell check changed files
+      - id: cspell
+        name: check changed files spelling
         exclude: |
           (?x)^(
-              .config/cspell.config.yaml|
+              \.cspell\.config\.yaml|
               include/xrpl/protocol_autogen/(transactions|ledger_entries)/.*
           )$
-      - id: cspell # Spell check the commit message
+      - id: cspell
         name: check commit message spelling
         args:
           - --no-must-find-files
           - --no-progress
           - --no-summary
-          - --files
-          - .git/COMMIT_EDITMSG
         stages: [commit-msg]
 
   - repo: local

From 6f0f5b8bb36c30c2578a62d1b2a5befa736da4c4 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 2 Jul 2026 17:26:09 +0100
Subject: [PATCH 039/100] chore: Make clang-tidy happy on macOS (#7701)

---
 BUILD.md                                           | 2 +-
 bin/pre-commit/clang_tidy_check.py                 | 8 +++++---
 include/xrpl/beast/unit_test/suite_list.h          | 6 +++---
 include/xrpl/ledger/ApplyView.h                    | 2 +-
 include/xrpl/ledger/ReadView.h                     | 2 +-
 include/xrpl/ledger/helpers/LendingHelpers.h       | 2 +-
 include/xrpl/protocol/AmountConversions.h          | 2 +-
 include/xrpl/tx/paths/detail/EitherAmount.h        | 2 +-
 src/libxrpl/ledger/helpers/NFTokenHelpers.cpp      | 2 +-
 src/libxrpl/ledger/helpers/OfferHelpers.cpp        | 2 +-
 src/libxrpl/ledger/helpers/VaultHelpers.cpp        | 2 +-
 src/libxrpl/protocol/Quality.cpp                   | 4 ++--
 src/libxrpl/protocol/STInteger.cpp                 | 2 +-
 src/libxrpl/protocol/STTx.cpp                      | 1 +
 src/libxrpl/protocol/STValidation.cpp              | 2 +-
 src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp | 2 +-
 src/test/app/PayChan_test.cpp                      | 2 +-
 src/test/app/Vault_test.cpp                        | 2 +-
 src/test/beast/beast_io_latency_probe_test.cpp     | 2 +-
 src/test/jtx/impl/amount.cpp                       | 2 +-
 src/test/rpc/DepositAuthorized_test.cpp            | 2 +-
 src/xrpld/app/ledger/detail/BuildLedger.cpp        | 4 ++--
 src/xrpld/app/ledger/detail/InboundLedger.cpp      | 4 ++--
 src/xrpld/app/ledger/detail/LedgerPersistence.cpp  | 4 ++--
 src/xrpld/app/ledger/detail/OpenLedger.cpp         | 2 +-
 src/xrpld/app/main/Application.cpp                 | 4 ++--
 src/xrpld/app/misc/FeeVoteImpl.cpp                 | 2 +-
 src/xrpld/overlay/detail/PeerImp.cpp               | 1 +
 28 files changed, 39 insertions(+), 35 deletions(-)

diff --git a/BUILD.md b/BUILD.md
index 6ccdde12d5..a15c94edc9 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -25,7 +25,7 @@ You can verify that the required tools are installed and runnable with:
 | ----------- | --------------- |
 | GCC         | 15.2            |
 | Clang       | 22              |
-| Apple Clang | 17              |
+| Apple Clang | 21              |
 | MSVC        | 19.44[^windows] |
 
 ## Operating Systems
diff --git a/bin/pre-commit/clang_tidy_check.py b/bin/pre-commit/clang_tidy_check.py
index d074c56acf..5b5792b405 100755
--- a/bin/pre-commit/clang_tidy_check.py
+++ b/bin/pre-commit/clang_tidy_check.py
@@ -17,9 +17,11 @@ import subprocess
 import sys
 from pathlib import Path
 
+CLANG_TIDY_VERSION = 22
+
 
 def find_run_clang_tidy() -> str | None:
-    for candidate in ("run-clang-tidy-21", "run-clang-tidy"):
+    for candidate in (f"run-clang-tidy-{CLANG_TIDY_VERSION}", "run-clang-tidy"):
         if path := shutil.which(candidate):
             return path
     return None
@@ -44,8 +46,8 @@ def main():
     run_clang_tidy = find_run_clang_tidy()
     if not run_clang_tidy:
         print(
-            "clang-tidy check failed: TIDY is enabled but neither "
-            "'run-clang-tidy-21' nor 'run-clang-tidy' was found in PATH.",
+            f"clang-tidy check failed: TIDY is enabled but neither "
+            f"'run-clang-tidy-{CLANG_TIDY_VERSION}' nor 'run-clang-tidy' was found in PATH.",
             file=sys.stderr,
         )
         return 1
diff --git a/include/xrpl/beast/unit_test/suite_list.h b/include/xrpl/beast/unit_test/suite_list.h
index cf9fb9c5b1..7dd0dd80f0 100644
--- a/include/xrpl/beast/unit_test/suite_list.h
+++ b/include/xrpl/beast/unit_test/suite_list.h
@@ -10,9 +10,9 @@
 #include 
 
 #include 
-#include 
-#include 
-#include 
+#include          // IWYU pragma: keep
+#include       // IWYU pragma: keep
+#include   // IWYU pragma: keep
 
 namespace beast::unit_test {
 
diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h
index 3bf5d479d1..e519013d9a 100644
--- a/include/xrpl/ledger/ApplyView.h
+++ b/include/xrpl/ledger/ApplyView.h
@@ -5,7 +5,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h
index 8bbd3e06cb..724533039b 100644
--- a/include/xrpl/ledger/ReadView.h
+++ b/include/xrpl/ledger/ReadView.h
@@ -7,7 +7,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h
index c21e5bf0ce..873abae272 100644
--- a/include/xrpl/ledger/helpers/LendingHelpers.h
+++ b/include/xrpl/ledger/helpers/LendingHelpers.h
@@ -8,7 +8,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h
index 66ada68d6f..3bcd80e827 100644
--- a/include/xrpl/protocol/AmountConversions.h
+++ b/include/xrpl/protocol/AmountConversions.h
@@ -13,7 +13,7 @@
 #include 
 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 
diff --git a/include/xrpl/tx/paths/detail/EitherAmount.h b/include/xrpl/tx/paths/detail/EitherAmount.h
index 68ad90d2d4..bc9488d5db 100644
--- a/include/xrpl/tx/paths/detail/EitherAmount.h
+++ b/include/xrpl/tx/paths/detail/EitherAmount.h
@@ -6,7 +6,7 @@
 #include   // IWYU pragma: keep
 #include 
 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 
diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
index af429358bb..6dca715a8c 100644
--- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
@@ -24,7 +24,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp b/src/libxrpl/ledger/helpers/OfferHelpers.cpp
index 5249870143..6e72b71564 100644
--- a/src/libxrpl/ledger/helpers/OfferHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp
@@ -6,7 +6,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include   // IWYU pragma: keep
 #include 
diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp
index 3a3a756499..b5b076d1cb 100644
--- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp
@@ -5,7 +5,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/protocol/Quality.cpp b/src/libxrpl/protocol/Quality.cpp
index 7ad426bef7..dde7921a76 100644
--- a/src/libxrpl/protocol/Quality.cpp
+++ b/src/libxrpl/protocol/Quality.cpp
@@ -1,12 +1,12 @@
 #include 
 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
 
 #include 
-#include 
+#include   // IWYU pragma: keep
 
 namespace xrpl {
 
diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp
index 5f3fb6ffa4..b65504dd5f 100644
--- a/src/libxrpl/protocol/STInteger.cpp
+++ b/src/libxrpl/protocol/STInteger.cpp
@@ -16,7 +16,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 
 namespace xrpl {
 
diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp
index cd2da12316..b3de717da8 100644
--- a/src/libxrpl/protocol/STTx.cpp
+++ b/src/libxrpl/protocol/STTx.cpp
@@ -596,6 +596,7 @@ STTx::getBatchTransactionIDs() const
     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_;
 }
 
diff --git a/src/libxrpl/protocol/STValidation.cpp b/src/libxrpl/protocol/STValidation.cpp
index 5eafb407ec..1656aad3a2 100644
--- a/src/libxrpl/protocol/STValidation.cpp
+++ b/src/libxrpl/protocol/STValidation.cpp
@@ -6,7 +6,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
index 3d30005876..ad91c55723 100644
--- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
@@ -11,7 +11,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp
index fd2c7790d5..f1a9506e0c 100644
--- a/src/test/app/PayChan_test.cpp
+++ b/src/test/app/PayChan_test.cpp
@@ -19,7 +19,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
index f6574872f9..3877e08f7e 100644
--- a/src/test/app/Vault_test.cpp
+++ b/src/test/app/Vault_test.cpp
@@ -7542,7 +7542,7 @@ class Vault_test : public beast::unit_test::Suite
         // Transaction fails if the data field is set, but is empty
         {
             testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty");
-            delTx[sfMemoData] = strHex(std::string(0, 'A'));
+            delTx[sfMemoData] = strHex(std::string());
             env(delTx, Ter(temMALFORMED));
             env.close();
         }
diff --git a/src/test/beast/beast_io_latency_probe_test.cpp b/src/test/beast/beast_io_latency_probe_test.cpp
index 9a93968dab..807ad81c49 100644
--- a/src/test/beast/beast_io_latency_probe_test.cpp
+++ b/src/test/beast/beast_io_latency_probe_test.cpp
@@ -8,7 +8,7 @@
 #include 
 #include 
 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include   // IWYU pragma: keep
 #include 
diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp
index b07703dace..f90102245a 100644
--- a/src/test/jtx/impl/amount.cpp
+++ b/src/test/jtx/impl/amount.cpp
@@ -12,7 +12,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/test/rpc/DepositAuthorized_test.cpp b/src/test/rpc/DepositAuthorized_test.cpp
index e6720602c9..6d6b54d70a 100644
--- a/src/test/rpc/DepositAuthorized_test.cpp
+++ b/src/test/rpc/DepositAuthorized_test.cpp
@@ -8,7 +8,7 @@
 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp
index 038a7be4b7..d11e0610ba 100644
--- a/src/xrpld/app/ledger/detail/BuildLedger.cpp
+++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp
@@ -13,10 +13,10 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 
diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp
index 4affffd1c1..4d34f60374 100644
--- a/src/xrpld/app/ledger/detail/InboundLedger.cpp
+++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp
@@ -21,11 +21,11 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp
index 3561b66951..6baf64e923 100644
--- a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp
@@ -8,9 +8,9 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 
 #include 
diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp
index 60599c80d3..4d8a37cdf1 100644
--- a/src/xrpld/app/ledger/detail/OpenLedger.cpp
+++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp
@@ -14,7 +14,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp
index b99c98afa1..594195b93e 100644
--- a/src/xrpld/app/main/Application.cpp
+++ b/src/xrpld/app/main/Application.cpp
@@ -79,11 +79,11 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp
index 363c17f4fa..76a4d8f186 100644
--- a/src/xrpld/app/misc/FeeVoteImpl.cpp
+++ b/src/xrpld/app/misc/FeeVoteImpl.cpp
@@ -8,7 +8,7 @@
 #include 
 #include 
 #include 
-#include 
+#include   // IWYU pragma: keep
 #include 
 #include   // IWYU pragma: keep
 #include 
diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp
index 5ac61cfd91..83e4d9c851 100644
--- a/src/xrpld/overlay/detail/PeerImp.cpp
+++ b/src/xrpld/overlay/detail/PeerImp.cpp
@@ -2134,6 +2134,7 @@ PeerImp::onValidatorListMessage(
             publisherListSequences_[pubKey] = applyResult.sequence;
         }
         break;
+        // NOLINTNEXTLINE(bugprone-branch-clone): identical to the next branch only in Release
         case ListDisposition::SameSequence:
         case ListDisposition::KnownSequence:
 #ifndef NDEBUG

From 41622b87aeeb6d5e9f03f12d0a6847e0a04e6174 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 2 Jul 2026 19:30:59 +0100
Subject: [PATCH 040/100] chore: Enable modernize-unary-static-assert (#7705)

---
 .clang-tidy                                   |   1 -
 include/xrpl/basics/base_uint.h               |   2 +-
 include/xrpl/beast/utility/Journal.h          |  48 ++--
 include/xrpl/ledger/CachedView.h              |   2 +-
 include/xrpl/ledger/detail/ReadViewFwdRange.h |   4 +-
 include/xrpl/protocol/STObject.h              |   2 +-
 include/xrpl/protocol/Serializer.h            |   2 +-
 src/libxrpl/protocol/Serializer.cpp           |   2 +-
 src/libxrpl/protocol/digest.cpp               |   6 +-
 src/test/app/PayStrand_test.cpp               |   2 +-
 src/test/basics/Buffer_test.cpp               |   4 +-
 src/test/jtx/Env_test.cpp                     |   8 +-
 src/test/protocol/Quality_test.cpp            |   4 +-
 src/test/protocol/STObject_test.cpp           |   6 +-
 src/test/protocol/SeqProxy_test.cpp           | 220 +++++++++---------
 src/test/shamap/SHAMap_test.cpp               |  88 +++----
 src/xrpld/app/misc/detail/TxQ.cpp             |   8 +-
 17 files changed, 203 insertions(+), 206 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index 84849db7a0..2b0b7e4418 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -80,7 +80,6 @@ Checks: "-*,
   -modernize-replace-random-shuffle,
   -modernize-return-braced-init-list,
   -modernize-shrink-to-fit,
-  -modernize-unary-static-assert,
   -modernize-use-auto,
   -modernize-use-bool-literals,
   -modernize-use-constraints,
diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h
index e6ca1993f9..c60fbf35b4 100644
--- a/include/xrpl/basics/base_uint.h
+++ b/include/xrpl/basics/base_uint.h
@@ -97,7 +97,7 @@ public:
     //
 
     static constexpr std::size_t kBytes = Bits / 8;
-    static_assert(sizeof(data_) == kBytes, "");
+    static_assert(sizeof(data_) == kBytes);
 
     using size_type = std::size_t;
     using difference_type = std::ptrdiff_t;
diff --git a/include/xrpl/beast/utility/Journal.h b/include/xrpl/beast/utility/Journal.h
index ac08b1384b..3de3cfb0e0 100644
--- a/include/xrpl/beast/utility/Journal.h
+++ b/include/xrpl/beast/utility/Journal.h
@@ -108,12 +108,12 @@ public:
     };
 
 #ifndef __INTELLISENSE__
-    static_assert(!std::is_default_constructible_v, "");
-    static_assert(!std::is_copy_constructible_v, "");
-    static_assert(!std::is_move_constructible_v, "");
-    static_assert(!std::is_copy_assignable_v, "");
-    static_assert(!std::is_move_assignable_v, "");
-    static_assert(std::is_nothrow_destructible_v, "");
+    static_assert(!std::is_default_constructible_v);
+    static_assert(!std::is_copy_constructible_v);
+    static_assert(!std::is_move_constructible_v);
+    static_assert(!std::is_copy_assignable_v);
+    static_assert(!std::is_move_assignable_v);
+    static_assert(std::is_nothrow_destructible_v);
 #endif
 
     /** Returns a Sink which does nothing. */
@@ -164,12 +164,12 @@ public:
     };
 
 #ifndef __INTELLISENSE__
-    static_assert(!std::is_default_constructible_v, "");
-    static_assert(std::is_copy_constructible_v, "");
-    static_assert(std::is_move_constructible_v, "");
-    static_assert(!std::is_copy_assignable_v, "");
-    static_assert(!std::is_move_assignable_v, "");
-    static_assert(std::is_nothrow_destructible_v, "");
+    static_assert(!std::is_default_constructible_v);
+    static_assert(std::is_copy_constructible_v);
+    static_assert(std::is_move_constructible_v);
+    static_assert(!std::is_copy_assignable_v);
+    static_assert(!std::is_move_assignable_v);
+    static_assert(std::is_nothrow_destructible_v);
 #endif
 
     //--------------------------------------------------------------------------
@@ -246,12 +246,12 @@ public:
     };
 
 #ifndef __INTELLISENSE__
-    static_assert(std::is_default_constructible_v, "");
-    static_assert(std::is_copy_constructible_v, "");
-    static_assert(std::is_move_constructible_v, "");
-    static_assert(!std::is_copy_assignable_v, "");
-    static_assert(!std::is_move_assignable_v, "");
-    static_assert(std::is_nothrow_destructible_v, "");
+    static_assert(std::is_default_constructible_v);
+    static_assert(std::is_copy_constructible_v);
+    static_assert(std::is_move_constructible_v);
+    static_assert(!std::is_copy_assignable_v);
+    static_assert(!std::is_move_assignable_v);
+    static_assert(std::is_nothrow_destructible_v);
 #endif
 
     //--------------------------------------------------------------------------
@@ -329,12 +329,12 @@ public:
 };
 
 #ifndef __INTELLISENSE__
-static_assert(!std::is_default_constructible_v, "");
-static_assert(std::is_copy_constructible_v, "");
-static_assert(std::is_move_constructible_v, "");
-static_assert(std::is_copy_assignable_v, "");
-static_assert(std::is_move_assignable_v, "");
-static_assert(std::is_nothrow_destructible_v, "");
+static_assert(!std::is_default_constructible_v);
+static_assert(std::is_copy_constructible_v);
+static_assert(std::is_move_constructible_v);
+static_assert(std::is_copy_assignable_v);
+static_assert(std::is_move_assignable_v);
+static_assert(std::is_nothrow_destructible_v);
 #endif
 
 //------------------------------------------------------------------------------
diff --git a/include/xrpl/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h
index f83c3e1297..1da3a67563 100644
--- a/include/xrpl/ledger/CachedView.h
+++ b/include/xrpl/ledger/CachedView.h
@@ -141,7 +141,7 @@ template 
 class CachedView : public detail::CachedViewImpl
 {
 private:
-    static_assert(std::is_base_of_v, "");
+    static_assert(std::is_base_of_v);
 
     std::shared_ptr sp_;
 
diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h
index 82d8b59c6a..19ac0698c2 100644
--- a/include/xrpl/ledger/detail/ReadViewFwdRange.h
+++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h
@@ -108,8 +108,8 @@ public:
         std::optional mutable cache_;
     };
 
-    static_assert(std::is_nothrow_move_constructible{}, "");
-    static_assert(std::is_nothrow_move_assignable{}, "");
+    static_assert(std::is_nothrow_move_constructible{});
+    static_assert(std::is_nothrow_move_assignable{});
 
     using const_iterator = Iterator;
 
diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h
index c254a37aaf..a60e8f7fe8 100644
--- a/include/xrpl/protocol/STObject.h
+++ b/include/xrpl/protocol/STObject.h
@@ -1241,7 +1241,7 @@ template 
 void
 STObject::setFieldUsingSetValue(SField const& field, V value)
 {
-    static_assert(!std::is_lvalue_reference_v, "");
+    static_assert(!std::is_lvalue_reference_v);
 
     STBase* rf = getPField(field, true);
 
diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h
index 1d0453d6aa..73bd9c8289 100644
--- a/include/xrpl/protocol/Serializer.h
+++ b/include/xrpl/protocol/Serializer.h
@@ -334,7 +334,7 @@ public:
     template 
     explicit SerialIter(std::uint8_t const (&data)[N]) : SerialIter(&data[0], N)
     {
-        static_assert(N > 0, "");
+        static_assert(N > 0);
     }
 
     [[nodiscard]] bool
diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp
index d500919df9..1eda04705f 100644
--- a/src/libxrpl/protocol/Serializer.cpp
+++ b/src/libxrpl/protocol/Serializer.cpp
@@ -444,7 +444,7 @@ template 
 T
 SerialIter::getRawHelper(int size)
 {
-    static_assert(std::is_same_v || std::is_same_v, "");
+    static_assert(std::is_same_v || std::is_same_v);
     if (remain_ < size)
         Throw("invalid SerialIter getRaw");
     T result(size);
diff --git a/src/libxrpl/protocol/digest.cpp b/src/libxrpl/protocol/digest.cpp
index 2e1b2b25cf..1d84f8779e 100644
--- a/src/libxrpl/protocol/digest.cpp
+++ b/src/libxrpl/protocol/digest.cpp
@@ -9,7 +9,7 @@ namespace xrpl {
 
 OpensslRipemd160Hasher::OpensslRipemd160Hasher()
 {
-    static_assert(sizeof(decltype(OpensslRipemd160Hasher::ctx_)) == sizeof(RIPEMD160_CTX), "");
+    static_assert(sizeof(decltype(OpensslRipemd160Hasher::ctx_)) == sizeof(RIPEMD160_CTX));
     auto const ctx = reinterpret_cast(ctx_);
     RIPEMD160_Init(ctx);
 }
@@ -34,7 +34,7 @@ operator result_type() noexcept
 
 OpensslSha512Hasher::OpensslSha512Hasher()
 {
-    static_assert(sizeof(decltype(OpensslSha512Hasher::ctx_)) == sizeof(SHA512_CTX), "");
+    static_assert(sizeof(decltype(OpensslSha512Hasher::ctx_)) == sizeof(SHA512_CTX));
     auto const ctx = reinterpret_cast(ctx_);
     SHA512_Init(ctx);
 }
@@ -59,7 +59,7 @@ operator result_type() noexcept
 
 OpensslSha256Hasher::OpensslSha256Hasher()
 {
-    static_assert(sizeof(decltype(OpensslSha256Hasher::ctx_)) == sizeof(SHA256_CTX), "");
+    static_assert(sizeof(decltype(OpensslSha256Hasher::ctx_)) == sizeof(SHA256_CTX));
     auto const ctx = reinterpret_cast(ctx_);
     SHA256_Init(ctx);
 }
diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp
index 11a0e5bab0..ddbcfe8b70 100644
--- a/src/test/app/PayStrand_test.cpp
+++ b/src/test/app/PayStrand_test.cpp
@@ -112,7 +112,7 @@ class ElementComboIter
         };
 
     std::uint16_t state_ = 0;
-    static_assert(safeCast(SB::Last) <= sizeof(decltype(state_)) * 8, "");
+    static_assert(safeCast(SB::Last) <= sizeof(decltype(state_)) * 8);
     STPathElement const* prev_ = nullptr;
     // disallow iss and cur to be specified with acc is specified (simplifies
     // some tests)
diff --git a/src/test/basics/Buffer_test.cpp b/src/test/basics/Buffer_test.cpp
index 816a697ca4..c748a3f9dd 100644
--- a/src/test/basics/Buffer_test.cpp
+++ b/src/test/basics/Buffer_test.cpp
@@ -103,8 +103,8 @@ struct Buffer_test : beast::unit_test::Suite
         {
             testcase("Move Construction / Assignment");
 
-            static_assert(std::is_nothrow_move_constructible_v, "");
-            static_assert(std::is_nothrow_move_assignable_v, "");
+            static_assert(std::is_nothrow_move_constructible_v);
+            static_assert(std::is_nothrow_move_assignable_v);
 
             {  // Move-construct from empty buf
                 Buffer x;
diff --git a/src/test/jtx/Env_test.cpp b/src/test/jtx/Env_test.cpp
index d82cb86b36..47cfb604f4 100644
--- a/src/test/jtx/Env_test.cpp
+++ b/src/test/jtx/Env_test.cpp
@@ -110,10 +110,10 @@ public:
         PrettyAmount(0u);  // NOLINT(bugprone-unused-raii)
         PrettyAmount(1u);  // NOLINT(bugprone-unused-raii)
         PrettyAmount(-1);  // NOLINT(bugprone-unused-raii)
-        static_assert(!std::is_trivially_constructible_v, "");
-        static_assert(!std::is_trivially_constructible_v, "");
-        static_assert(!std::is_trivially_constructible_v, "");
-        static_assert(!std::is_trivially_constructible_v, "");
+        static_assert(!std::is_trivially_constructible_v);
+        static_assert(!std::is_trivially_constructible_v);
+        static_assert(!std::is_trivially_constructible_v);
+        static_assert(!std::is_trivially_constructible_v);
 
         try
         {
diff --git a/src/test/protocol/Quality_test.cpp b/src/test/protocol/Quality_test.cpp
index df95cea8ef..db81886e94 100644
--- a/src/test/protocol/Quality_test.cpp
+++ b/src/test/protocol/Quality_test.cpp
@@ -24,7 +24,7 @@ public:
     static STAmount
     amount(Integer integer, std::enable_if_t>* = 0)
     {
-        static_assert(std::is_integral_v, "");
+        static_assert(std::is_integral_v);
         return STAmount(integer, false);
     }
 
@@ -32,7 +32,7 @@ public:
     static STAmount
     amount(Integer integer, std::enable_if_t>* = 0)
     {
-        static_assert(std::is_integral_v, "");
+        static_assert(std::is_integral_v);
         if (integer < 0)
             return STAmount(-integer, true);
         return STAmount(integer, false);
diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp
index b823b24962..8b7db632c5 100644
--- a/src/test/protocol/STObject_test.cpp
+++ b/src/test/protocol/STObject_test.cpp
@@ -356,8 +356,7 @@ public:
         {
             STObject st(sfGeneric);
             auto const v = ~st[~sf1Outer];
-            static_assert(
-                std::is_same_v, std::optional>, "");
+            static_assert(std::is_same_v, std::optional>);
         }
 
         // UDT scalar fields
@@ -431,8 +430,7 @@ public:
             BEAST_EXPECT(cst[~sf]->size() == 2);  // NOLINT(bugprone-unchecked-optional-access)
             BEAST_EXPECT(cst[sf][0] == 1);
             BEAST_EXPECT(cst[sf][1] == 2);
-            static_assert(
-                std::is_same_v const&>, "");
+            static_assert(std::is_same_v const&>);
         }
 
         // Default by reference field
diff --git a/src/test/protocol/SeqProxy_test.cpp b/src/test/protocol/SeqProxy_test.cpp
index 90345ddc22..44aab41992 100644
--- a/src/test/protocol/SeqProxy_test.cpp
+++ b/src/test/protocol/SeqProxy_test.cpp
@@ -81,128 +81,128 @@ struct SeqProxy_test : public beast::unit_test::Suite
         static constexpr SeqProxy kTicBig{kTicket, kUintMax};
 
         // Verify operation of value(), isSeq() and isTicket().
-        static_assert(expectValues(kSeqZero, 0, kSeq), "");
-        static_assert(expectValues(kSeqSmall, 1, kSeq), "");
-        static_assert(expectValues(kSeqMiD0, 2, kSeq), "");
-        static_assert(expectValues(kSeqMiD1, 2, kSeq), "");
-        static_assert(expectValues(kSeqBig, kUintMax, kSeq), "");
+        static_assert(expectValues(kSeqZero, 0, kSeq));
+        static_assert(expectValues(kSeqSmall, 1, kSeq));
+        static_assert(expectValues(kSeqMiD0, 2, kSeq));
+        static_assert(expectValues(kSeqMiD1, 2, kSeq));
+        static_assert(expectValues(kSeqBig, kUintMax, kSeq));
 
-        static_assert(expectValues(kTicZero, 0, kTicket), "");
-        static_assert(expectValues(kTicSmall, 1, kTicket), "");
-        static_assert(expectValues(kTicMid0, 2, kTicket), "");
-        static_assert(expectValues(kTicMid1, 2, kTicket), "");
-        static_assert(expectValues(kTicBig, kUintMax, kTicket), "");
+        static_assert(expectValues(kTicZero, 0, kTicket));
+        static_assert(expectValues(kTicSmall, 1, kTicket));
+        static_assert(expectValues(kTicMid0, 2, kTicket));
+        static_assert(expectValues(kTicMid1, 2, kTicket));
+        static_assert(expectValues(kTicBig, kUintMax, kTicket));
 
         // Verify expected behavior of comparison operators.
-        static_assert(expectEq(kSeqZero, kSeqZero), "");
-        static_assert(expectLt(kSeqZero, kSeqSmall), "");
-        static_assert(expectLt(kSeqZero, kSeqMiD0), "");
-        static_assert(expectLt(kSeqZero, kSeqMiD1), "");
-        static_assert(expectLt(kSeqZero, kSeqBig), "");
-        static_assert(expectLt(kSeqZero, kTicZero), "");
-        static_assert(expectLt(kSeqZero, kTicSmall), "");
-        static_assert(expectLt(kSeqZero, kTicMid0), "");
-        static_assert(expectLt(kSeqZero, kTicMid1), "");
-        static_assert(expectLt(kSeqZero, kTicBig), "");
+        static_assert(expectEq(kSeqZero, kSeqZero));
+        static_assert(expectLt(kSeqZero, kSeqSmall));
+        static_assert(expectLt(kSeqZero, kSeqMiD0));
+        static_assert(expectLt(kSeqZero, kSeqMiD1));
+        static_assert(expectLt(kSeqZero, kSeqBig));
+        static_assert(expectLt(kSeqZero, kTicZero));
+        static_assert(expectLt(kSeqZero, kTicSmall));
+        static_assert(expectLt(kSeqZero, kTicMid0));
+        static_assert(expectLt(kSeqZero, kTicMid1));
+        static_assert(expectLt(kSeqZero, kTicBig));
 
-        static_assert(expectGt(kSeqSmall, kSeqZero), "");
-        static_assert(expectEq(kSeqSmall, kSeqSmall), "");
-        static_assert(expectLt(kSeqSmall, kSeqMiD0), "");
-        static_assert(expectLt(kSeqSmall, kSeqMiD1), "");
-        static_assert(expectLt(kSeqSmall, kSeqBig), "");
-        static_assert(expectLt(kSeqSmall, kTicZero), "");
-        static_assert(expectLt(kSeqSmall, kTicSmall), "");
-        static_assert(expectLt(kSeqSmall, kTicMid0), "");
-        static_assert(expectLt(kSeqSmall, kTicMid1), "");
-        static_assert(expectLt(kSeqSmall, kTicBig), "");
+        static_assert(expectGt(kSeqSmall, kSeqZero));
+        static_assert(expectEq(kSeqSmall, kSeqSmall));
+        static_assert(expectLt(kSeqSmall, kSeqMiD0));
+        static_assert(expectLt(kSeqSmall, kSeqMiD1));
+        static_assert(expectLt(kSeqSmall, kSeqBig));
+        static_assert(expectLt(kSeqSmall, kTicZero));
+        static_assert(expectLt(kSeqSmall, kTicSmall));
+        static_assert(expectLt(kSeqSmall, kTicMid0));
+        static_assert(expectLt(kSeqSmall, kTicMid1));
+        static_assert(expectLt(kSeqSmall, kTicBig));
 
-        static_assert(expectGt(kSeqMiD0, kSeqZero), "");
-        static_assert(expectGt(kSeqMiD0, kSeqSmall), "");
-        static_assert(expectEq(kSeqMiD0, kSeqMiD0), "");
-        static_assert(expectEq(kSeqMiD0, kSeqMiD1), "");
-        static_assert(expectLt(kSeqMiD0, kSeqBig), "");
-        static_assert(expectLt(kSeqMiD0, kTicZero), "");
-        static_assert(expectLt(kSeqMiD0, kTicSmall), "");
-        static_assert(expectLt(kSeqMiD0, kTicMid0), "");
-        static_assert(expectLt(kSeqMiD0, kTicMid1), "");
-        static_assert(expectLt(kSeqMiD0, kTicBig), "");
+        static_assert(expectGt(kSeqMiD0, kSeqZero));
+        static_assert(expectGt(kSeqMiD0, kSeqSmall));
+        static_assert(expectEq(kSeqMiD0, kSeqMiD0));
+        static_assert(expectEq(kSeqMiD0, kSeqMiD1));
+        static_assert(expectLt(kSeqMiD0, kSeqBig));
+        static_assert(expectLt(kSeqMiD0, kTicZero));
+        static_assert(expectLt(kSeqMiD0, kTicSmall));
+        static_assert(expectLt(kSeqMiD0, kTicMid0));
+        static_assert(expectLt(kSeqMiD0, kTicMid1));
+        static_assert(expectLt(kSeqMiD0, kTicBig));
 
-        static_assert(expectGt(kSeqMiD1, kSeqZero), "");
-        static_assert(expectGt(kSeqMiD1, kSeqSmall), "");
-        static_assert(expectEq(kSeqMiD1, kSeqMiD0), "");
-        static_assert(expectEq(kSeqMiD1, kSeqMiD1), "");
-        static_assert(expectLt(kSeqMiD1, kSeqBig), "");
-        static_assert(expectLt(kSeqMiD1, kTicZero), "");
-        static_assert(expectLt(kSeqMiD1, kTicSmall), "");
-        static_assert(expectLt(kSeqMiD1, kTicMid0), "");
-        static_assert(expectLt(kSeqMiD1, kTicMid1), "");
-        static_assert(expectLt(kSeqMiD1, kTicBig), "");
+        static_assert(expectGt(kSeqMiD1, kSeqZero));
+        static_assert(expectGt(kSeqMiD1, kSeqSmall));
+        static_assert(expectEq(kSeqMiD1, kSeqMiD0));
+        static_assert(expectEq(kSeqMiD1, kSeqMiD1));
+        static_assert(expectLt(kSeqMiD1, kSeqBig));
+        static_assert(expectLt(kSeqMiD1, kTicZero));
+        static_assert(expectLt(kSeqMiD1, kTicSmall));
+        static_assert(expectLt(kSeqMiD1, kTicMid0));
+        static_assert(expectLt(kSeqMiD1, kTicMid1));
+        static_assert(expectLt(kSeqMiD1, kTicBig));
 
-        static_assert(expectGt(kSeqBig, kSeqZero), "");
-        static_assert(expectGt(kSeqBig, kSeqSmall), "");
-        static_assert(expectGt(kSeqBig, kSeqMiD0), "");
-        static_assert(expectGt(kSeqBig, kSeqMiD1), "");
-        static_assert(expectEq(kSeqBig, kSeqBig), "");
-        static_assert(expectLt(kSeqBig, kTicZero), "");
-        static_assert(expectLt(kSeqBig, kTicSmall), "");
-        static_assert(expectLt(kSeqBig, kTicMid0), "");
-        static_assert(expectLt(kSeqBig, kTicMid1), "");
-        static_assert(expectLt(kSeqBig, kTicBig), "");
+        static_assert(expectGt(kSeqBig, kSeqZero));
+        static_assert(expectGt(kSeqBig, kSeqSmall));
+        static_assert(expectGt(kSeqBig, kSeqMiD0));
+        static_assert(expectGt(kSeqBig, kSeqMiD1));
+        static_assert(expectEq(kSeqBig, kSeqBig));
+        static_assert(expectLt(kSeqBig, kTicZero));
+        static_assert(expectLt(kSeqBig, kTicSmall));
+        static_assert(expectLt(kSeqBig, kTicMid0));
+        static_assert(expectLt(kSeqBig, kTicMid1));
+        static_assert(expectLt(kSeqBig, kTicBig));
 
-        static_assert(expectGt(kTicZero, kSeqZero), "");
-        static_assert(expectGt(kTicZero, kSeqSmall), "");
-        static_assert(expectGt(kTicZero, kSeqMiD0), "");
-        static_assert(expectGt(kTicZero, kSeqMiD1), "");
-        static_assert(expectGt(kTicZero, kSeqBig), "");
-        static_assert(expectEq(kTicZero, kTicZero), "");
-        static_assert(expectLt(kTicZero, kTicSmall), "");
-        static_assert(expectLt(kTicZero, kTicMid0), "");
-        static_assert(expectLt(kTicZero, kTicMid1), "");
-        static_assert(expectLt(kTicZero, kTicBig), "");
+        static_assert(expectGt(kTicZero, kSeqZero));
+        static_assert(expectGt(kTicZero, kSeqSmall));
+        static_assert(expectGt(kTicZero, kSeqMiD0));
+        static_assert(expectGt(kTicZero, kSeqMiD1));
+        static_assert(expectGt(kTicZero, kSeqBig));
+        static_assert(expectEq(kTicZero, kTicZero));
+        static_assert(expectLt(kTicZero, kTicSmall));
+        static_assert(expectLt(kTicZero, kTicMid0));
+        static_assert(expectLt(kTicZero, kTicMid1));
+        static_assert(expectLt(kTicZero, kTicBig));
 
-        static_assert(expectGt(kTicSmall, kSeqZero), "");
-        static_assert(expectGt(kTicSmall, kSeqSmall), "");
-        static_assert(expectGt(kTicSmall, kSeqMiD0), "");
-        static_assert(expectGt(kTicSmall, kSeqMiD1), "");
-        static_assert(expectGt(kTicSmall, kSeqBig), "");
-        static_assert(expectGt(kTicSmall, kTicZero), "");
-        static_assert(expectEq(kTicSmall, kTicSmall), "");
-        static_assert(expectLt(kTicSmall, kTicMid0), "");
-        static_assert(expectLt(kTicSmall, kTicMid1), "");
-        static_assert(expectLt(kTicSmall, kTicBig), "");
+        static_assert(expectGt(kTicSmall, kSeqZero));
+        static_assert(expectGt(kTicSmall, kSeqSmall));
+        static_assert(expectGt(kTicSmall, kSeqMiD0));
+        static_assert(expectGt(kTicSmall, kSeqMiD1));
+        static_assert(expectGt(kTicSmall, kSeqBig));
+        static_assert(expectGt(kTicSmall, kTicZero));
+        static_assert(expectEq(kTicSmall, kTicSmall));
+        static_assert(expectLt(kTicSmall, kTicMid0));
+        static_assert(expectLt(kTicSmall, kTicMid1));
+        static_assert(expectLt(kTicSmall, kTicBig));
 
-        static_assert(expectGt(kTicMid0, kSeqZero), "");
-        static_assert(expectGt(kTicMid0, kSeqSmall), "");
-        static_assert(expectGt(kTicMid0, kSeqMiD0), "");
-        static_assert(expectGt(kTicMid0, kSeqMiD1), "");
-        static_assert(expectGt(kTicMid0, kSeqBig), "");
-        static_assert(expectGt(kTicMid0, kTicZero), "");
-        static_assert(expectGt(kTicMid0, kTicSmall), "");
-        static_assert(expectEq(kTicMid0, kTicMid0), "");
-        static_assert(expectEq(kTicMid0, kTicMid1), "");
-        static_assert(expectLt(kTicMid0, kTicBig), "");
+        static_assert(expectGt(kTicMid0, kSeqZero));
+        static_assert(expectGt(kTicMid0, kSeqSmall));
+        static_assert(expectGt(kTicMid0, kSeqMiD0));
+        static_assert(expectGt(kTicMid0, kSeqMiD1));
+        static_assert(expectGt(kTicMid0, kSeqBig));
+        static_assert(expectGt(kTicMid0, kTicZero));
+        static_assert(expectGt(kTicMid0, kTicSmall));
+        static_assert(expectEq(kTicMid0, kTicMid0));
+        static_assert(expectEq(kTicMid0, kTicMid1));
+        static_assert(expectLt(kTicMid0, kTicBig));
 
-        static_assert(expectGt(kTicMid1, kSeqZero), "");
-        static_assert(expectGt(kTicMid1, kSeqSmall), "");
-        static_assert(expectGt(kTicMid1, kSeqMiD0), "");
-        static_assert(expectGt(kTicMid1, kSeqMiD1), "");
-        static_assert(expectGt(kTicMid1, kSeqBig), "");
-        static_assert(expectGt(kTicMid1, kTicZero), "");
-        static_assert(expectGt(kTicMid1, kTicSmall), "");
-        static_assert(expectEq(kTicMid1, kTicMid0), "");
-        static_assert(expectEq(kTicMid1, kTicMid1), "");
-        static_assert(expectLt(kTicMid1, kTicBig), "");
+        static_assert(expectGt(kTicMid1, kSeqZero));
+        static_assert(expectGt(kTicMid1, kSeqSmall));
+        static_assert(expectGt(kTicMid1, kSeqMiD0));
+        static_assert(expectGt(kTicMid1, kSeqMiD1));
+        static_assert(expectGt(kTicMid1, kSeqBig));
+        static_assert(expectGt(kTicMid1, kTicZero));
+        static_assert(expectGt(kTicMid1, kTicSmall));
+        static_assert(expectEq(kTicMid1, kTicMid0));
+        static_assert(expectEq(kTicMid1, kTicMid1));
+        static_assert(expectLt(kTicMid1, kTicBig));
 
-        static_assert(expectGt(kTicBig, kSeqZero), "");
-        static_assert(expectGt(kTicBig, kSeqSmall), "");
-        static_assert(expectGt(kTicBig, kSeqMiD0), "");
-        static_assert(expectGt(kTicBig, kSeqMiD1), "");
-        static_assert(expectGt(kTicBig, kSeqBig), "");
-        static_assert(expectGt(kTicBig, kTicZero), "");
-        static_assert(expectGt(kTicBig, kTicSmall), "");
-        static_assert(expectGt(kTicBig, kTicMid0), "");
-        static_assert(expectGt(kTicBig, kTicMid1), "");
-        static_assert(expectEq(kTicBig, kTicBig), "");
+        static_assert(expectGt(kTicBig, kSeqZero));
+        static_assert(expectGt(kTicBig, kSeqSmall));
+        static_assert(expectGt(kTicBig, kSeqMiD0));
+        static_assert(expectGt(kTicBig, kSeqMiD1));
+        static_assert(expectGt(kTicBig, kSeqBig));
+        static_assert(expectGt(kTicBig, kTicZero));
+        static_assert(expectGt(kTicBig, kTicSmall));
+        static_assert(expectGt(kTicBig, kTicMid0));
+        static_assert(expectGt(kTicBig, kTicMid1));
+        static_assert(expectEq(kTicBig, kTicBig));
 
         // Verify streaming.
         BEAST_EXPECT(streamTest(kSeqZero));
diff --git a/src/test/shamap/SHAMap_test.cpp b/src/test/shamap/SHAMap_test.cpp
index ab7e01e3af..5ff5ef5e05 100644
--- a/src/test/shamap/SHAMap_test.cpp
+++ b/src/test/shamap/SHAMap_test.cpp
@@ -26,57 +26,57 @@
 namespace xrpl::tests {
 
 #ifndef __INTELLISENSE__
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(!std::is_default_constructible{}, "");
-static_assert(!std::is_copy_constructible{}, "");
-static_assert(!std::is_copy_assignable{}, "");
-static_assert(!std::is_move_constructible{}, "");
-static_assert(!std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(!std::is_default_constructible{});
+static_assert(!std::is_copy_constructible{});
+static_assert(!std::is_copy_assignable{});
+static_assert(!std::is_move_constructible{});
+static_assert(!std::is_move_assignable{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(std::is_copy_constructible{}, "");
-static_assert(std::is_copy_assignable{}, "");
-static_assert(std::is_move_constructible{}, "");
-static_assert(std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(std::is_copy_constructible{});
+static_assert(std::is_copy_assignable{});
+static_assert(std::is_move_constructible{});
+static_assert(std::is_move_assignable{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(!std::is_default_constructible{}, "");
-static_assert(!std::is_copy_constructible{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(!std::is_default_constructible{});
+static_assert(!std::is_copy_constructible{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(std::is_default_constructible{}, "");
-static_assert(std::is_copy_constructible{}, "");
-static_assert(std::is_copy_assignable{}, "");
-static_assert(std::is_move_constructible{}, "");
-static_assert(std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(std::is_default_constructible{});
+static_assert(std::is_copy_constructible{});
+static_assert(std::is_copy_assignable{});
+static_assert(std::is_move_constructible{});
+static_assert(std::is_move_assignable{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(std::is_default_constructible{}, "");
-static_assert(std::is_copy_constructible{}, "");
-static_assert(std::is_copy_assignable{}, "");
-static_assert(std::is_move_constructible{}, "");
-static_assert(std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(std::is_default_constructible{});
+static_assert(std::is_copy_constructible{});
+static_assert(std::is_copy_assignable{});
+static_assert(std::is_move_constructible{});
+static_assert(std::is_move_assignable{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(!std::is_default_constructible{}, "");
-static_assert(!std::is_copy_constructible{}, "");
-static_assert(!std::is_copy_assignable{}, "");
-static_assert(!std::is_move_constructible{}, "");
-static_assert(!std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(!std::is_default_constructible{});
+static_assert(!std::is_copy_constructible{});
+static_assert(!std::is_copy_assignable{});
+static_assert(!std::is_move_constructible{});
+static_assert(!std::is_move_assignable{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(!std::is_default_constructible{}, "");
-static_assert(!std::is_copy_constructible{}, "");
-static_assert(!std::is_copy_assignable{}, "");
-static_assert(!std::is_move_constructible{}, "");
-static_assert(!std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(!std::is_default_constructible{});
+static_assert(!std::is_copy_constructible{});
+static_assert(!std::is_copy_assignable{});
+static_assert(!std::is_move_constructible{});
+static_assert(!std::is_move_assignable{});
 
-static_assert(std::is_nothrow_destructible{}, "");
-static_assert(!std::is_default_constructible{}, "");
-static_assert(!std::is_copy_constructible{}, "");
-static_assert(!std::is_copy_assignable{}, "");
-static_assert(!std::is_move_constructible{}, "");
-static_assert(!std::is_move_assignable{}, "");
+static_assert(std::is_nothrow_destructible{});
+static_assert(!std::is_default_constructible{});
+static_assert(!std::is_copy_constructible{});
+static_assert(!std::is_copy_assignable{});
+static_assert(!std::is_move_constructible{});
+static_assert(!std::is_move_assignable{});
 #endif
 
 inline bool
diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp
index 7e57302d23..5d1020d296 100644
--- a/src/xrpld/app/misc/detail/TxQ.cpp
+++ b/src/xrpld/app/misc/detail/TxQ.cpp
@@ -228,11 +228,11 @@ static_assert(sumOfFirstSquares(1).second == 1);
 static_assert(sumOfFirstSquares(2).first);
 static_assert(sumOfFirstSquares(2).second == 5);
 
-static_assert(sumOfFirstSquares(0x1FFFFF).first, "");
-static_assert(sumOfFirstSquares(0x1FFFFF).second == 0x2AAAA8AAAAB00000ul, "");
+static_assert(sumOfFirstSquares(0x1FFFFF).first);
+static_assert(sumOfFirstSquares(0x1FFFFF).second == 0x2AAAA8AAAAB00000ul);
 
-static_assert(!sumOfFirstSquares(0x200000).first, "");
-static_assert(sumOfFirstSquares(0x200000).second == std::numeric_limits::max(), "");
+static_assert(!sumOfFirstSquares(0x200000).first);
+static_assert(sumOfFirstSquares(0x200000).second == std::numeric_limits::max());
 
 }  // namespace detail
 

From 6003fd03fc11c7675a9f03ba791e63aaa0e8265a Mon Sep 17 00:00:00 2001
From: yinyiqian1 
Date: Thu, 2 Jul 2026 14:37:37 -0400
Subject: [PATCH 041/100] feat: Enable ConfidentialTransfer and BatchV1_1
 (#7698)

---
 include/xrpl/protocol/detail/features.macro | 4 ++--
 src/test/app/Delegate_test.cpp              | 2 --
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro
index a7a62f0322..3f078561a3 100644
--- a/include/xrpl/protocol/detail/features.macro
+++ b/include/xrpl/protocol/detail/features.macro
@@ -15,9 +15,9 @@
 // Add new amendments to the top of this list.
 // Keep it sorted in reverse chronological order.
 
-XRPL_FEATURE(BatchV1_1,                   Supported::No,  VoteBehavior::DefaultNo)
+XRPL_FEATURE(BatchV1_1,                   Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(LendingProtocolV1_1,         Supported::No,  VoteBehavior::DefaultNo)
-XRPL_FEATURE(ConfidentialTransfer,        Supported::No,  VoteBehavior::DefaultNo)
+XRPL_FEATURE(ConfidentialTransfer,        Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_3_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FIX    (Cleanup3_2_0,                Supported::Yes, VoteBehavior::DefaultNo)
 XRPL_FEATURE(MPTokensV2,                  Supported::No,  VoteBehavior::DefaultNo)
diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp
index 02ee0750d5..6f1d2ce7de 100644
--- a/src/test/app/Delegate_test.cpp
+++ b/src/test/app/Delegate_test.cpp
@@ -2747,8 +2747,6 @@ class Delegate_test : public beast::unit_test::Suite
         // DO NOT modify expectedDelegableCount unless all scenarios, including
         // edge cases, have been fully tested and verified.
         // ====================================================================
-        // Includes the five confidential MPT transaction types, which are
-        // explicitly marked Delegable in transactions.macro.
         std::size_t const expectedDelegableCount = 56;
 
         BEAST_EXPECTS(

From 3d847f2a6039be6837d7ed8329a9a8496283dc4a Mon Sep 17 00:00:00 2001
From: Bart 
Date: Thu, 2 Jul 2026 14:46:27 -0400
Subject: [PATCH 042/100] build: Add protobuf dependencies to Nix (#7706)

Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
---
 nix/packages.nix | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/nix/packages.nix b/nix/packages.nix
index 6202168733..bcadfe7456 100644
--- a/nix/packages.nix
+++ b/nix/packages.nix
@@ -32,6 +32,15 @@ in
     perl # needed for openssl
     pkg-config
     pre-commit
+    # protoc generates the Go gRPC bindings and embeds its own version string into every committed
+    # .pb.go file. To allow CI to verify those files with a plain `git diff`, we pin the version to
+    # `protobuf_34` rather than the rolling `protobuf` to keep regeneration reproducible across the
+    # Nix frequently changing unstable channel. The protoc-gen-go* plugins have no versioned
+    # attributes in nixpkgs; protoc-gen-go's version is in turn constrained by the go.mod require
+    # on google.golang.org/protobuf.
+    protobuf_34 # provides protoc
+    protoc-gen-go # protoc plugin for the Go message bindings
+    protoc-gen-go-grpc # protoc plugin for the Go gRPC service stubs
     python3
     runClangTidy
     vim

From 7ba1d76d0543c94e74df3c634b11448b293cdaf8 Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Thu, 2 Jul 2026 21:02:55 +0100
Subject: [PATCH 043/100] chore: Enable modernize-use-auto (#7707)

---
 .clang-tidy                                      |  1 -
 include/xrpl/beast/hash/hash_append.h            |  2 +-
 include/xrpl/beast/utility/rngfill.h             |  4 ++--
 include/xrpl/core/JobTypes.h                     |  2 +-
 include/xrpl/ledger/helpers/EscrowHelpers.h      |  2 +-
 include/xrpl/nodestore/detail/codec.h            |  8 ++++----
 include/xrpl/nodestore/detail/varint.h           |  4 ++--
 include/xrpl/protocol/Quality.h                  |  2 +-
 include/xrpl/protocol/STBitString.h              |  2 +-
 include/xrpl/protocol/STInteger.h                |  2 +-
 src/libxrpl/basics/Number.cpp                    |  2 +-
 src/libxrpl/beast/insight/StatsDCollector.cpp    |  4 ++--
 src/libxrpl/core/detail/JobQueue.cpp             | 10 +++++-----
 src/libxrpl/crypto/RFC1751.cpp                   |  2 +-
 src/libxrpl/json/json_reader.cpp                 |  2 +-
 src/libxrpl/json/json_value.cpp                  | 16 ++++++++--------
 src/libxrpl/json/json_writer.cpp                 |  6 +++---
 src/libxrpl/nodestore/DecodedBlob.cpp            |  2 +-
 src/libxrpl/nodestore/backend/MemoryFactory.cpp  |  2 +-
 src/libxrpl/nodestore/backend/NuDBFactory.cpp    |  2 +-
 src/libxrpl/nodestore/backend/RocksDBFactory.cpp |  2 +-
 src/libxrpl/protocol/IOUAmount.cpp               |  2 +-
 src/libxrpl/protocol/MPTIssue.cpp                |  3 +--
 src/libxrpl/protocol/NFTokenID.cpp               |  3 +--
 src/libxrpl/protocol/STAmount.cpp                |  2 +-
 src/libxrpl/protocol/STBlob.cpp                  |  2 +-
 src/libxrpl/protocol/STCurrency.cpp              |  2 +-
 src/libxrpl/protocol/STIssue.cpp                 |  2 +-
 src/libxrpl/protocol/STNumber.cpp                |  2 +-
 src/libxrpl/protocol/STObject.cpp                | 10 +++++-----
 src/libxrpl/protocol/STPathSet.cpp               |  4 ++--
 src/libxrpl/protocol/STVector256.cpp             |  2 +-
 src/libxrpl/protocol/STXChainBridge.cpp          |  2 +-
 .../tx/transactors/escrow/EscrowCreate.cpp       |  2 +-
 src/test/app/ConfidentialTransfer_test.cpp       |  2 +-
 src/test/app/Loan_test.cpp                       |  2 +-
 src/test/app/Offer_test.cpp                      |  2 +-
 src/test/beast/LexicalCast_test.cpp              |  6 +++---
 .../beast/aged_associative_container_test.cpp    |  2 +-
 src/test/shamap/FetchPack_test.cpp               |  2 +-
 src/xrpld/app/consensus/RCLConsensus.cpp         |  2 +-
 src/xrpld/app/ledger/detail/InboundLedgers.cpp   |  2 +-
 src/xrpld/app/main/Application.cpp               |  2 +-
 src/xrpld/app/misc/NetworkOPs.cpp                |  4 ++--
 src/xrpld/app/misc/detail/Transaction.cpp        |  2 +-
 src/xrpld/app/misc/detail/TxQ.cpp                | 10 +++++-----
 src/xrpld/app/rdb/backend/detail/Node.cpp        |  2 +-
 src/xrpld/core/detail/Config.cpp                 |  2 +-
 src/xrpld/rpc/CTID.h                             |  6 +++---
 src/xrpld/rpc/handlers/orderbook/BookOffers.cpp  |  2 +-
 50 files changed, 82 insertions(+), 85 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index 2b0b7e4418..6a24cbc8d3 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -80,7 +80,6 @@ Checks: "-*,
   -modernize-replace-random-shuffle,
   -modernize-return-braced-init-list,
   -modernize-shrink-to-fit,
-  -modernize-use-auto,
   -modernize-use-bool-literals,
   -modernize-use-constraints,
   -modernize-use-default-member-init,
diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h
index 0b36d4c983..5f77f41e5d 100644
--- a/include/xrpl/beast/hash/hash_append.h
+++ b/include/xrpl/beast/hash/hash_append.h
@@ -26,7 +26,7 @@ template 
 inline void
 reverseBytes(T& t)
 {
-    unsigned char* bytes =
+    auto* bytes =
         static_cast(std::memmove(std::addressof(t), std::addressof(t), sizeof(T)));
     for (unsigned i = 0; i < sizeof(T) / 2; ++i)
         std::swap(bytes[i], bytes[sizeof(T) - 1 - i]);
diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h
index 0fc3ffe0d0..ee7a5e2434 100644
--- a/include/xrpl/beast/utility/rngfill.h
+++ b/include/xrpl/beast/utility/rngfill.h
@@ -14,7 +14,7 @@ rngfill(void* const buffer, std::size_t const bytes, Generator& g)
     using result_type = Generator::result_type;
     constexpr std::size_t kResultSize = sizeof(result_type);
 
-    std::uint8_t* const bufferStart = static_cast(buffer);
+    auto* const bufferStart = static_cast(buffer);
     std::size_t const completeIterations = bytes / kResultSize;
     std::size_t const bytesRemaining = bytes % kResultSize;
 
@@ -42,7 +42,7 @@ rngfill(std::array& a, Generator& g)
 {
     using result_type = Generator::result_type;
     auto i = N / sizeof(result_type);
-    result_type* p = reinterpret_cast(a.data());
+    auto* p = reinterpret_cast(a.data());
     while (i--)
         *p++ = g();
 }
diff --git a/include/xrpl/core/JobTypes.h b/include/xrpl/core/JobTypes.h
index f338b19f6c..cc2f3ecbf5 100644
--- a/include/xrpl/core/JobTypes.h
+++ b/include/xrpl/core/JobTypes.h
@@ -118,7 +118,7 @@ public:
     [[nodiscard]] JobTypeInfo const&
     get(JobType jt) const
     {
-        Map::const_iterator const iter(map.find(jt));
+        auto const iter = map.find(jt);
         XRPL_ASSERT(iter != map.end(), "xrpl::JobTypes::get : valid input");
 
         if (iter != map.end())
diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h
index 001dc9cb25..d173c33c56 100644
--- a/include/xrpl/ledger/helpers/EscrowHelpers.h
+++ b/include/xrpl/ledger/helpers/EscrowHelpers.h
@@ -54,7 +54,7 @@ escrowUnlockApplyHelper(
     bool createAsset,
     beast::Journal journal)
 {
-    Issue const& issue = amount.get();
+    auto const& issue = amount.get();
     Keylet const trustLineKey = keylet::trustLine(receiver, issue);
     bool const recvLow = issuer > receiver;
     bool const senderIssuer = issuer == sender;
diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h
index 56e1fbcf73..2f69d532be 100644
--- a/include/xrpl/nodestore/detail/codec.h
+++ b/include/xrpl/nodestore/detail/codec.h
@@ -62,7 +62,7 @@ lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf)
     std::array::kMax> vi{};
     auto const n = writeVarint(vi.data(), inSize);
     auto const outMax = LZ4_compressBound(inSize);
-    std::uint8_t* out = reinterpret_cast(bf(n + outMax));
+    auto* out = reinterpret_cast(bf(n + outMax));
     result.first = out;
     std::memcpy(out, vi.data(), n);
     auto const outSize = LZ4_compress_default(
@@ -90,7 +90,7 @@ nodeobjectDecompress(void const* in, std::size_t inSize, BufferFactory&& bf)
 {
     using namespace nudb::detail;
 
-    std::uint8_t const* p = reinterpret_cast(in);
+    auto const* p = reinterpret_cast(in);
     std::size_t type = 0;
     auto const vn = readVarint(p, inSize, type);
     if (vn == 0)
@@ -237,7 +237,7 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
                 auto const vs = sizeVarint(type);
                 result.second = vs + field::size +  // mask
                     (n * 32);                                      // hashes
-                std::uint8_t* out = reinterpret_cast(bf(result.second));
+                auto* out = reinterpret_cast(bf(result.second));
                 result.first = out;
                 ostream os(out, result.second);
                 write(os, type);
@@ -249,7 +249,7 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
             auto const type = 3U;
             auto const vs = sizeVarint(type);
             result.second = vs + (n * 32);  // hashes
-            std::uint8_t* out = reinterpret_cast(bf(result.second));
+            auto* out = reinterpret_cast(bf(result.second));
             result.first = out;
             ostream os(out, result.second);
             write(os, type);
diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h
index 21a13bd6de..6102c0cf2d 100644
--- a/include/xrpl/nodestore/detail/varint.h
+++ b/include/xrpl/nodestore/detail/varint.h
@@ -39,7 +39,7 @@ readVarint(void const* buf, std::size_t buflen, std::size_t& t)
     if (buflen == 0)
         return 0;
     t = 0;
-    std::uint8_t const* p = reinterpret_cast(buf);
+    auto const* p = reinterpret_cast(buf);
     std::size_t n = 0;
     while (p[n] & 0x80)
     {
@@ -86,7 +86,7 @@ std::size_t
 writeVarint(void* p0, std::size_t v)
 {
     // NOLINTNEXTLINE(misc-const-correctness)
-    std::uint8_t* p = reinterpret_cast(p0);
+    auto* p = reinterpret_cast(p0);
     do
     {
         std::uint8_t d = v % 127;
diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h
index 6bb2033dc0..de61d79ca5 100644
--- a/include/xrpl/protocol/Quality.h
+++ b/include/xrpl/protocol/Quality.h
@@ -282,7 +282,7 @@ public:
         auto const maxVMantissa = mantissa(maxV);
         auto const expDiff = exponent(maxV) - exponent(minV);
 
-        double const minVD = static_cast(minVMantissa);
+        auto const minVD = static_cast(minVMantissa);
         double const maxVD =
             (expDiff != 0) ? maxVMantissa * pow(10, expDiff) : static_cast(maxVMantissa);
 
diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h
index 4f25eccca6..6f71f48f25 100644
--- a/include/xrpl/protocol/STBitString.h
+++ b/include/xrpl/protocol/STBitString.h
@@ -148,7 +148,7 @@ template 
 bool
 STBitString::isEquivalent(STBase const& t) const
 {
-    STBitString const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return v && (value_ == v->value_);
 }
 
diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h
index 1c7ac98f3e..951c4fc52f 100644
--- a/include/xrpl/protocol/STInteger.h
+++ b/include/xrpl/protocol/STInteger.h
@@ -115,7 +115,7 @@ template 
 inline bool
 STInteger::isEquivalent(STBase const& t) const
 {
-    STInteger const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return v && (value_ == v->value_);
 }
 
diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp
index 23e913bbdc..4d7a821040 100644
--- a/src/libxrpl/basics/Number.cpp
+++ b/src/libxrpl/basics/Number.cpp
@@ -876,7 +876,7 @@ Number::operator/=(Number const& y)
     int const ds = (dp ? -1 : 1);
     // Create the denominator as 128-bit unsigned, since that's what we
     // need to work with.
-    uint128_t const dm = static_cast(y.mantissa_);
+    auto const dm = static_cast(y.mantissa_);
     auto const de = y.exponent_;
 
     auto const& range = kRange.get();
diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp
index 0d0e013274..1da5315de1 100644
--- a/src/libxrpl/beast/insight/StatsDCollector.cpp
+++ b/src/libxrpl/beast/insight/StatsDCollector.cpp
@@ -640,14 +640,14 @@ StatsDGaugeImpl::doIncrement(GaugeImpl::difference_type amount)
 
     if (amount > 0)
     {
-        GaugeImpl::value_type const d(static_cast(amount));
+        auto const d = static_cast(amount);
         value += (d >= std::numeric_limits::max() - value_)
             ? std::numeric_limits::max() - value_
             : d;
     }
     else if (amount < 0)
     {
-        GaugeImpl::value_type const d(static_cast(-amount));
+        auto const d = static_cast(-amount);
         value = (d >= value) ? 0 : value - d;
     }
 
diff --git a/src/libxrpl/core/detail/JobQueue.cpp b/src/libxrpl/core/detail/JobQueue.cpp
index ffb91db72d..f773270af3 100644
--- a/src/libxrpl/core/detail/JobQueue.cpp
+++ b/src/libxrpl/core/detail/JobQueue.cpp
@@ -122,7 +122,7 @@ JobQueue::getJobCount(JobType t) const
 {
     std::scoped_lock const lock(mutex_);
 
-    JobDataMap::const_iterator const c = jobData_.find(t);
+    auto const c = jobData_.find(t);
 
     return (c == jobData_.end()) ? 0 : c->second.waiting;
 }
@@ -132,7 +132,7 @@ JobQueue::getJobCountTotal(JobType t) const
 {
     std::scoped_lock const lock(mutex_);
 
-    JobDataMap::const_iterator const c = jobData_.find(t);
+    auto const c = jobData_.find(t);
 
     return (c == jobData_.end()) ? 0 : (c->second.waiting + c->second.running);
 }
@@ -157,7 +157,7 @@ JobQueue::getJobCountGE(JobType t) const
 std::unique_ptr
 JobQueue::makeLoadEvent(JobType t, std::string const& name)
 {
-    JobDataMap::iterator const iter(jobData_.find(t));
+    auto const iter = jobData_.find(t);
     XRPL_ASSERT(iter != jobData_.end(), "xrpl::JobQueue::makeLoadEvent : valid job type input");
 
     if (iter == jobData_.end())
@@ -172,7 +172,7 @@ JobQueue::addLoadEvents(JobType t, int count, std::chrono::milliseconds elapsed)
     if (isStopped())
         logicError("JobQueue::addLoadEvents() called after JobQueue stopped");
 
-    JobDataMap::iterator const iter(jobData_.find(t));
+    auto const iter = jobData_.find(t);
     XRPL_ASSERT(iter != jobData_.end(), "xrpl::JobQueue::addLoadEvents : valid job type input");
     iter->second.load().addSamples(count, elapsed);
 }
@@ -250,7 +250,7 @@ JobQueue::rendezvous()
 JobTypeData&
 JobQueue::getJobTypeData(JobType type)
 {
-    JobDataMap::iterator const c(jobData_.find(type));
+    auto const c = jobData_.find(type);
     XRPL_ASSERT(c != jobData_.end(), "xrpl::JobQueue::getJobTypeData : valid job type input");
 
     // NIKB: This is ugly and I hate it. We must remove JtInvalid completely
diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp
index 16482945d2..41e29ee00c 100644
--- a/src/libxrpl/crypto/RFC1751.cpp
+++ b/src/libxrpl/crypto/RFC1751.cpp
@@ -434,7 +434,7 @@ RFC1751::getWordFromBlob(void const* blob, size_t bytes)
     // This is a simple implementation of the Jenkins one-at-a-time hash
     // algorithm:
     // http://en.wikipedia.org/wiki/Jenkins_hash_function#one-at-a-time
-    unsigned char const* data = static_cast(blob);
+    auto const* data = static_cast(blob);
     std::uint32_t hash = 0;
 
     for (size_t i = 0; i < bytes; ++i)
diff --git a/src/libxrpl/json/json_reader.cpp b/src/libxrpl/json/json_reader.cpp
index 3786f51fdd..45e448c480 100644
--- a/src/libxrpl/json/json_reader.cpp
+++ b/src/libxrpl/json/json_reader.cpp
@@ -908,7 +908,7 @@ Reader::getFormattedErrorMessages() const
 {
     std::string formattedMessage;
 
-    for (Errors::const_iterator itError = errors_.begin(); itError != errors_.end(); ++itError)
+    for (auto itError = errors_.begin(); itError != errors_.end(); ++itError)
     {
         ErrorInfo const& error = *itError;
         formattedMessage += "* " + getLocationLineAndColumn(error.token.start) + "\n";
diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp
index 074a88428c..7218b8c6cf 100644
--- a/src/libxrpl/json/json_value.cpp
+++ b/src/libxrpl/json/json_value.cpp
@@ -802,7 +802,7 @@ Value::size() const
         case ValueType::Array:  // size of the array is highest index + 1
             if (!value_.mapVal->empty())
             {
-                ObjectValues::const_iterator itLast = value_.mapVal->end();
+                auto itLast = value_.mapVal->end();
                 --itLast;
                 return (*itLast).first.index() + 1;
             }
@@ -866,7 +866,7 @@ Value::operator[](UInt index)
         *this = Value(ValueType::Array);
 
     CZString const key(index);
-    ObjectValues::iterator it = value_.mapVal->lower_bound(key);
+    auto it = value_.mapVal->lower_bound(key);
 
     if (it != value_.mapVal->end() && (*it).first == key)
         return (*it).second;
@@ -887,7 +887,7 @@ Value::operator[](UInt index) const
         return kNull;
 
     CZString const key(index);
-    ObjectValues::const_iterator const it = value_.mapVal->find(key);
+    auto const it = value_.mapVal->find(key);
 
     if (it == value_.mapVal->end())
         return kNull;
@@ -915,7 +915,7 @@ Value::resolveReference(char const* key, bool isStatic)
         key,
         isStatic ? CZString::DuplicationPolicy::NoDuplication
                  : CZString::DuplicationPolicy::DuplicateOnCopy);
-    ObjectValues::iterator it = value_.mapVal->lower_bound(actualKey);
+    auto it = value_.mapVal->lower_bound(actualKey);
 
     if (it != value_.mapVal->end() && (*it).first == actualKey)
         return (*it).second;
@@ -950,7 +950,7 @@ Value::operator[](char const* key) const
         return kNull;
 
     CZString const actualKey(key, CZString::DuplicationPolicy::NoDuplication);
-    ObjectValues::const_iterator const it = value_.mapVal->find(actualKey);
+    auto const it = value_.mapVal->find(actualKey);
 
     if (it == value_.mapVal->end())
         return kNull;
@@ -1018,7 +1018,7 @@ Value::removeMember(char const* key)
         return kNull;
 
     CZString const actualKey(key, CZString::DuplicationPolicy::NoDuplication);
-    ObjectValues::iterator const it = value_.mapVal->find(actualKey);
+    auto const it = value_.mapVal->find(actualKey);
 
     if (it == value_.mapVal->end())
         return kNull;
@@ -1068,8 +1068,8 @@ Value::getMemberNames() const
 
     Members members;
     members.reserve(value_.mapVal->size());
-    ObjectValues::const_iterator it = value_.mapVal->begin();
-    ObjectValues::const_iterator const itEnd = value_.mapVal->end();
+    auto it = value_.mapVal->begin();
+    auto const itEnd = value_.mapVal->end();
 
     for (; it != itEnd; ++it)
         members.emplace_back((*it).first.cStr());
diff --git a/src/libxrpl/json/json_writer.cpp b/src/libxrpl/json/json_writer.cpp
index 4c38bdcf92..c9f8cb688b 100644
--- a/src/libxrpl/json/json_writer.cpp
+++ b/src/libxrpl/json/json_writer.cpp
@@ -232,7 +232,7 @@ FastWriter::writeValue(Value const& value)
             Value::Members members(value.getMemberNames());
             document_ += "{";
 
-            for (Value::Members::iterator it = members.begin(); it != members.end(); ++it)
+            for (auto it = members.begin(); it != members.end(); ++it)
             {
                 std::string const& name = *it;
 
@@ -310,7 +310,7 @@ StyledWriter::writeValue(Value const& value)
             {
                 writeWithIndent("{");
                 indent();
-                Value::Members::iterator it = members.begin();
+                auto it = members.begin();
 
                 while (true)
                 {
@@ -545,7 +545,7 @@ StyledStreamWriter::writeValue(Value const& value)
             {
                 writeWithIndent("{");
                 indent();
-                Value::Members::iterator it = members.begin();
+                auto it = members.begin();
 
                 while (true)
                 {
diff --git a/src/libxrpl/nodestore/DecodedBlob.cpp b/src/libxrpl/nodestore/DecodedBlob.cpp
index fb7569bd8c..fe07252f23 100644
--- a/src/libxrpl/nodestore/DecodedBlob.cpp
+++ b/src/libxrpl/nodestore/DecodedBlob.cpp
@@ -33,7 +33,7 @@ DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes)
 
     if (valueBytes > 8)
     {
-        unsigned char const* byte = static_cast(value);
+        auto const* byte = static_cast(value);
         objectType_ = safeCast(byte[8]);
     }
 
diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp b/src/libxrpl/nodestore/backend/MemoryFactory.cpp
index 70578c8613..22557d652e 100644
--- a/src/libxrpl/nodestore/backend/MemoryFactory.cpp
+++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp
@@ -136,7 +136,7 @@ public:
 
         std::scoped_lock const _(db_->mutex);
 
-        Map::iterator const iter = db_->table.find(hash);
+        auto const iter = db_->table.find(hash);
         if (iter == db_->table.end())
         {
             pObject->reset();
diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp
index 749d4020b5..38ea34258f 100644
--- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp
+++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp
@@ -367,7 +367,7 @@ private:
 
         try
         {
-            std::size_t const parsedBlockSize = beast::lexicalCastThrow(blockSizeStr);
+            auto const parsedBlockSize = beast::lexicalCastThrow(blockSizeStr);
 
             // Validate: must be power of 2 between 4K and 32K
             if (parsedBlockSize < 4096 || parsedBlockSize > 32768 ||
diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
index 69a648f2f4..bcf4ba4a49 100644
--- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
+++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
@@ -80,7 +80,7 @@ public:
     void
     StartThread(void (*f)(void*), void* a) override
     {
-        ThreadParams* const p(new ThreadParams(f, a));
+        auto* const p = new ThreadParams(f, a);
         EnvWrapper::StartThread(&RocksDBEnv::threadEntry, p);
     }
 };
diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp
index acbf6724e1..111edbf8b5 100644
--- a/src/libxrpl/protocol/IOUAmount.cpp
+++ b/src/libxrpl/protocol/IOUAmount.cpp
@@ -181,7 +181,7 @@ mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundU
             hasRem = bool(sav - low * kPowerTable[mustShrink]);
     }
 
-    std::int64_t mantissa = low.convert_to();
+    auto mantissa = low.convert_to();
 
     // normalize before rounding
     if (neg)
diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp
index e9ec852d1d..35ee743b2a 100644
--- a/src/libxrpl/protocol/MPTIssue.cpp
+++ b/src/libxrpl/protocol/MPTIssue.cpp
@@ -31,8 +31,7 @@ MPTIssue::getIssuer() const
     // MPTID is concatenation of sequence + account
     static_assert(sizeof(MPTID) == (sizeof(std::uint32_t) + sizeof(AccountID)));
     // copy from id skipping the sequence
-    AccountID const* account =
-        reinterpret_cast(mptID_.data() + sizeof(std::uint32_t));
+    auto const* account = reinterpret_cast(mptID_.data() + sizeof(std::uint32_t));
 
     return *account;
 }
diff --git a/src/libxrpl/protocol/NFTokenID.cpp b/src/libxrpl/protocol/NFTokenID.cpp
index cd2c46b66b..729f052101 100644
--- a/src/libxrpl/protocol/NFTokenID.cpp
+++ b/src/libxrpl/protocol/NFTokenID.cpp
@@ -72,8 +72,7 @@ getNFTokenIDFromPage(TxMeta const& transactionMeta)
             // However, there will always be NFTs listed in the final fields,
             // as xrpld outputs all fields in final fields even if they were
             // not changed.
-            STObject const& previousFields =
-                node.peekAtField(sfPreviousFields).downcast();
+            auto const& previousFields = node.peekAtField(sfPreviousFields).downcast();
             if (!previousFields.isFieldPresent(sfNFTokens))
                 continue;
 
diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp
index 748d00f25a..212c34322b 100644
--- a/src/libxrpl/protocol/STAmount.cpp
+++ b/src/libxrpl/protocol/STAmount.cpp
@@ -789,7 +789,7 @@ STAmount::add(Serializer& s) const
 bool
 STAmount::isEquivalent(STBase const& t) const
 {
-    STAmount const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (*v == *this);
 }
 
diff --git a/src/libxrpl/protocol/STBlob.cpp b/src/libxrpl/protocol/STBlob.cpp
index 3f44c9b529..14d09b633c 100644
--- a/src/libxrpl/protocol/STBlob.cpp
+++ b/src/libxrpl/protocol/STBlob.cpp
@@ -53,7 +53,7 @@ STBlob::add(Serializer& s) const
 bool
 STBlob::isEquivalent(STBase const& t) const
 {
-    STBlob const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (value_ == v->value_);
 }
 
diff --git a/src/libxrpl/protocol/STCurrency.cpp b/src/libxrpl/protocol/STCurrency.cpp
index 9b761864d9..fc8bd756ed 100644
--- a/src/libxrpl/protocol/STCurrency.cpp
+++ b/src/libxrpl/protocol/STCurrency.cpp
@@ -56,7 +56,7 @@ STCurrency::add(Serializer& s) const
 bool
 STCurrency::isEquivalent(STBase const& t) const
 {
-    STCurrency const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (*v == *this);
 }
 
diff --git a/src/libxrpl/protocol/STIssue.cpp b/src/libxrpl/protocol/STIssue.cpp
index c9b8109e32..10403d2c50 100644
--- a/src/libxrpl/protocol/STIssue.cpp
+++ b/src/libxrpl/protocol/STIssue.cpp
@@ -107,7 +107,7 @@ STIssue::add(Serializer& s) const
 bool
 STIssue::isEquivalent(STBase const& t) const
 {
-    STIssue const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (*v == *this);
 }
 
diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp
index fcc0077f6b..79f7655869 100644
--- a/src/libxrpl/protocol/STNumber.cpp
+++ b/src/libxrpl/protocol/STNumber.cpp
@@ -142,7 +142,7 @@ STNumber::isEquivalent(STBase const& t) const
 {
     XRPL_ASSERT(
         t.getSType() == this->getSType(), "xrpl::STNumber::isEquivalent : field type match");
-    STNumber const& v = dynamic_cast(t);
+    auto const& v = dynamic_cast(t);
     return value_ == v;
 }
 
diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp
index c493ba65af..ea8553dc4f 100644
--- a/src/libxrpl/protocol/STObject.cpp
+++ b/src/libxrpl/protocol/STObject.cpp
@@ -338,7 +338,7 @@ STObject::getText() const
 bool
 STObject::isEquivalent(STBase const& t) const
 {
-    STObject const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
 
     if (v == nullptr)
         return false;
@@ -474,7 +474,7 @@ STObject::peekFieldArray(SField const& field)
 bool
 STObject::setFlag(std::uint32_t f)
 {
-    STUInt32* t = dynamic_cast(getPField(sfFlags, true));
+    auto* t = dynamic_cast(getPField(sfFlags, true));
 
     if (t == nullptr)
         return false;
@@ -486,7 +486,7 @@ STObject::setFlag(std::uint32_t f)
 bool
 STObject::clearFlag(std::uint32_t f)
 {
-    STUInt32* t = dynamic_cast(getPField(sfFlags));
+    auto* t = dynamic_cast(getPField(sfFlags));
 
     if (t == nullptr)
         return false;
@@ -504,7 +504,7 @@ STObject::isFlag(std::uint32_t f) const
 std::uint32_t
 STObject::getFlags(void) const
 {
-    STUInt32 const* t = dynamic_cast(peekAtPField(sfFlags));
+    auto const* t = dynamic_cast(peekAtPField(sfFlags));
 
     if (t == nullptr)
         return 0;
@@ -651,7 +651,7 @@ Blob
 STObject::getFieldVL(SField const& field) const
 {
     STBlob const empty;
-    STBlob const& b = getFieldByConstRef(field, empty);
+    auto const& b = getFieldByConstRef(field, empty);
     return Blob(b.data(), b.data() + b.size());
 }
 
diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp
index cb70ac36fe..d61f17ecc6 100644
--- a/src/libxrpl/protocol/STPathSet.cpp
+++ b/src/libxrpl/protocol/STPathSet.cpp
@@ -127,7 +127,7 @@ STPathSet::assembleAdd(STPath const& base, STPathElement const& tail)
 {  // assemble base+tail and add it to the set if it's not a duplicate
     value_.push_back(base);
 
-    std::vector::reverse_iterator it = value_.rbegin();
+    auto it = value_.rbegin();
 
     STPath& newPath = *it;
     newPath.pushBack(tail);
@@ -146,7 +146,7 @@ STPathSet::assembleAdd(STPath const& base, STPathElement const& tail)
 bool
 STPathSet::isEquivalent(STBase const& t) const
 {
-    STPathSet const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (value_ == v->value_);
 }
 
diff --git a/src/libxrpl/protocol/STVector256.cpp b/src/libxrpl/protocol/STVector256.cpp
index 7aca309667..877e34c73c 100644
--- a/src/libxrpl/protocol/STVector256.cpp
+++ b/src/libxrpl/protocol/STVector256.cpp
@@ -68,7 +68,7 @@ STVector256::add(Serializer& s) const
 bool
 STVector256::isEquivalent(STBase const& t) const
 {
-    STVector256 const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (value_ == v->value_);
 }
 
diff --git a/src/libxrpl/protocol/STXChainBridge.cpp b/src/libxrpl/protocol/STXChainBridge.cpp
index ce6ad2368a..005c9ccbce 100644
--- a/src/libxrpl/protocol/STXChainBridge.cpp
+++ b/src/libxrpl/protocol/STXChainBridge.cpp
@@ -168,7 +168,7 @@ STXChainBridge::getSType() const
 bool
 STXChainBridge::isEquivalent(STBase const& t) const
 {
-    STXChainBridge const* v = dynamic_cast(&t);
+    auto const* v = dynamic_cast(&t);
     return (v != nullptr) && (*v == *this);
 }
 
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
index 0d83885349..531098bd1c 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
@@ -194,7 +194,7 @@ escrowCreatePreclaimHelper(
     AccountID const& dest,
     STAmount const& amount)
 {
-    Issue const& issue = amount.get();
+    auto const& issue = amount.get();
     AccountID const& issuer = amount.getIssuer();
     // If the issuer is the same as the account, return tecNO_PERMISSION
     if (issuer == account)
diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp
index e6faf0a2ae..d3e0182db5 100644
--- a/src/test/app/ConfidentialTransfer_test.cpp
+++ b/src/test/app/ConfidentialTransfer_test.cpp
@@ -6920,7 +6920,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
         auto& mptAlice = confEnv.mpt;
 
         uint64_t const sendAmount = 10;
-        uint64_t const negativeRemaining = static_cast(-10);  // 0xFFFFFFFFFFFFFFF6
+        auto const negativeRemaining = static_cast(-10);  // 0xFFFFFFFFFFFFFFF6
 
         ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
 
diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp
index 56a7cab47a..3e62af48ff 100644
--- a/src/test/app/Loan_test.cpp
+++ b/src/test/app/Loan_test.cpp
@@ -8651,7 +8651,7 @@ protected:
         Account const borrower("borrower");
 
         // Determine all the random parameters at once
-        AssetType const assetType = static_cast(assetDist_(engine_));
+        auto const assetType = static_cast(assetDist_(engine_));
         auto const principalRequest = principalDist_(engine_);
         TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)};
         auto const serviceFee = serviceFeeDist_(engine_);
diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp
index 804bd9b86c..2724a6474b 100644
--- a/src/test/app/Offer_test.cpp
+++ b/src/test/app/Offer_test.cpp
@@ -2195,7 +2195,7 @@ public:
         jtx::Account const& account,
         jtx::PrettyAmount const& expectBalance)
     {
-        Issue const& issue = expectBalance.value().get();
+        auto const& issue = expectBalance.value().get();
         auto const sleTrust = env.le(keylet::trustLine(account.id(), issue));
         BEAST_EXPECT(sleTrust);
         if (sleTrust)
diff --git a/src/test/beast/LexicalCast_test.cpp b/src/test/beast/LexicalCast_test.cpp
index f988aa03cf..b1d37daab8 100644
--- a/src/test/beast/LexicalCast_test.cpp
+++ b/src/test/beast/LexicalCast_test.cpp
@@ -24,7 +24,7 @@ public:
     testInteger(IntType in)
     {
         std::string s;
-        IntType out = static_cast(~in);  // Ensure out != in
+        auto out = static_cast(~in);  // Ensure out != in
 
         expect(lexicalCastChecked(s, in));
         expect(lexicalCastChecked(out, s));
@@ -42,7 +42,7 @@ public:
 
             for (int i = 0; i < 1000; ++i)
             {
-                IntType const value(nextRandomInt(r));
+                auto const value = nextRandomInt(r);
                 testInteger(value);
             }
         }
@@ -229,7 +229,7 @@ public:
 
         while (i <= std::numeric_limits::max())
         {
-            std::int16_t const j = static_cast(i);
+            auto const j = static_cast(i);
 
             auto actual = std::to_string(j);
 
diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp
index 3dbaf74040..df61824c99 100644
--- a/src/test/beast/aged_associative_container_test.cpp
+++ b/src/test/beast/aged_associative_container_test.cpp
@@ -1391,7 +1391,7 @@ AgedAssociativeContainerTestBase::reverseFillAgedContainer(Container& c, Values
     // c.clock() returns an abstract_clock, so dynamic_cast to ManualClock.
     // VFALCO NOTE This is sketchy
     using ManualClock = TestTraitsBase::ManualClock;
-    ManualClock& clk(dynamic_cast(c.clock()));
+    auto& clk = dynamic_cast(c.clock());
     clk.set(0);
 
     Values rev(values);
diff --git a/src/test/shamap/FetchPack_test.cpp b/src/test/shamap/FetchPack_test.cpp
index b2fc185b31..bcc5129f01 100644
--- a/src/test/shamap/FetchPack_test.cpp
+++ b/src/test/shamap/FetchPack_test.cpp
@@ -68,7 +68,7 @@ public:
         [[nodiscard]] std::optional
         getNode(SHAMapHash const& nodeHash) const override
         {
-            Map::iterator const it = map.find(nodeHash);
+            auto const it = map.find(nodeHash);
             if (it == map.end())
             {
                 JLOG(journal.fatal()) << "Test filter missing node";
diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp
index a7c2b26c04..2abc881adc 100644
--- a/src/xrpld/app/consensus/RCLConsensus.cpp
+++ b/src/xrpld/app/consensus/RCLConsensus.cpp
@@ -690,7 +690,7 @@ RCLConsensus::Adaptor::doAccept(
 
         JLOG(j_.info()) << "We closed at " << closeTime.time_since_epoch().count();
         using usec64_t = std::chrono::duration;
-        usec64_t closeTotal = std::chrono::duration_cast(closeTime.time_since_epoch());
+        auto closeTotal = std::chrono::duration_cast(closeTime.time_since_epoch());
         int closeCount = 1;
 
         for (auto const& [t, v] : rawCloseTimes.peers)
diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp
index 95e3e46f73..07daa7560e 100644
--- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp
+++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp
@@ -378,7 +378,7 @@ public:
 
         {
             ScopedLockType const sl(lock_);
-            MapType::iterator it(ledgers_.begin());
+            auto it = ledgers_.begin();
             total = ledgers_.size();
 
             stuffToSweep.reserve(total);
diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp
index 594195b93e..d329475874 100644
--- a/src/xrpld/app/main/Application.cpp
+++ b/src/xrpld/app/main/Application.cpp
@@ -2178,7 +2178,7 @@ fixConfigPorts(Config& config, Endpoints const& endpoints)
         auto const optPort = section.get(Keys::kPort);
         if (optPort)
         {
-            std::uint16_t const port = beast::lexicalCast(*optPort);
+            auto const port = beast::lexicalCast(*optPort);
             if (port == 0u)
                 section.set(Keys::kPort, std::to_string(ep.port()));
         }
diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp
index 82445f3625..d1f794b74d 100644
--- a/src/xrpld/app/misc/NetworkOPs.cpp
+++ b/src/xrpld/app/misc/NetworkOPs.cpp
@@ -4392,7 +4392,7 @@ NetworkOPsImp::findRpcSub(std::string const& strUrl)
 {
     std::scoped_lock const sl(subLock_);
 
-    subRpcMapType::iterator const it = rpcSubMap_.find(strUrl);
+    auto const it = rpcSubMap_.find(strUrl);
 
     if (it != rpcSubMap_.end())
         return it->second;
@@ -4827,7 +4827,7 @@ NetworkOPsImp::StateAccounting::json(json::Value& obj) const
     counters[static_cast(mode)].dur += current;
 
     obj[jss::state_accounting] = json::ValueType::Object;
-    for (std::size_t i = static_cast(OperatingMode::DISCONNECTED);
+    for (auto i = static_cast(OperatingMode::DISCONNECTED);
          i <= static_cast(OperatingMode::FULL);
          ++i)
     {
diff --git a/src/xrpld/app/misc/detail/Transaction.cpp b/src/xrpld/app/misc/detail/Transaction.cpp
index 2c55c474eb..e29181bfe9 100644
--- a/src/xrpld/app/misc/detail/Transaction.cpp
+++ b/src/xrpld/app/misc/detail/Transaction.cpp
@@ -103,7 +103,7 @@ Transaction::transactionFromSQL(
     Blob const& rawTxn,
     Application& app)
 {
-    std::uint32_t const inLedger = rangeCheckedCast(ledgerSeq.value_or(0));
+    auto const inLedger = rangeCheckedCast(ledgerSeq.value_or(0));
 
     SerialIter it(makeSlice(rawTxn));
     auto txn = std::make_shared(it);
diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp
index 5d1020d296..f6d00974b4 100644
--- a/src/xrpld/app/misc/detail/TxQ.cpp
+++ b/src/xrpld/app/misc/detail/TxQ.cpp
@@ -788,7 +788,7 @@ TxQ::apply(
     std::scoped_lock const lock(mutex_);
 
     // accountIter is not const because it may be updated further down.
-    AccountMap::iterator accountIter = byAccount_.find(account);
+    auto accountIter = byAccount_.find(account);
     bool const accountIsInQueue = accountIter != byAccount_.end();
 
     // _If_ the account is in the queue, then ignore any sequence-based
@@ -817,7 +817,7 @@ TxQ::apply(
 
         // Find the first transaction in the queue that we might apply.
         TxQAccount::TxMap& acctTxs = accountIter->second.transactions;
-        TxQAccount::TxMap::iterator const firstIter = acctTxs.lower_bound(acctSeqProx);
+        auto const firstIter = acctTxs.lower_bound(acctSeqProx);
 
         if (firstIter == acctTxs.end())
         {
@@ -996,7 +996,7 @@ TxQ::apply(
 
             // Find the entry in the queue that precedes the new
             // transaction, if one does.
-            TxQAccount::TxMap::const_iterator const prevIter = txQAcct.getPrevTx(txSeqProx);
+            auto const prevIter = txQAcct.getPrevTx(txSeqProx);
 
             // Does the new transaction go to the front of the queue?
             // This can happen if:
@@ -1615,7 +1615,7 @@ TxQ::nextQueuableSeqImpl(SLE::const_ref sleAccount, std::scoped_lock
     // Ignore any sequence-based queued transactions that slipped into the
     // ledger while we were not watching.  This does actually happen in the
     // wild, but it's uncommon.
-    TxQAccount::TxMap::const_iterator txIter = acctTxs.lower_bound(acctSeqProx);
+    auto txIter = acctTxs.lower_bound(acctSeqProx);
 
     if (txIter == acctTxs.end() || !txIter->first.isSeq() || txIter->first != acctSeqProx)
     {
@@ -1700,7 +1700,7 @@ TxQ::tryDirectApply(
             // queue then remove the replaced transaction.
             std::scoped_lock const lock(mutex_);
 
-            AccountMap::iterator const accountIter = byAccount_.find(account);
+            auto const accountIter = byAccount_.find(account);
             if (accountIter != byAccount_.end())
             {
                 TxQAccount& txQAcct = accountIter->second;
diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp
index 6e92a0de60..f8dc4a6981 100644
--- a/src/xrpld/app/rdb/backend/detail/Node.cpp
+++ b/src/xrpld/app/rdb/backend/detail/Node.cpp
@@ -1273,7 +1273,7 @@ getTransaction(
         if (!ledgerSeq)
             return std::pair{std::move(txn), nullptr};
 
-        std::uint32_t const inLedger = rangeCheckedCast(ledgerSeq.value());
+        auto const inLedger = rangeCheckedCast(ledgerSeq.value());
 
         auto txMeta = std::make_shared(id, inLedger, rawMeta);
 
diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp
index 523cde743a..07c780118a 100644
--- a/src/xrpld/core/detail/Config.cpp
+++ b/src/xrpld/core/detail/Config.cpp
@@ -926,7 +926,7 @@ Config::loadFromString(std::string const& fileContents)
                 ", must be: [0-9]+ [minutes|hours|days|weeks]");
         }
 
-        std::uint32_t const duration = beast::lexicalCastThrow(match[1].str());
+        auto const duration = beast::lexicalCastThrow(match[1].str());
 
         if (boost::iequals(match[2], "minutes"))
         {
diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h
index c2133a19b5..7566e0e143 100644
--- a/src/xrpld/rpc/CTID.h
+++ b/src/xrpld/rpc/CTID.h
@@ -104,9 +104,9 @@ decodeCTID(T const ctid) noexcept
     if ((ctidValue & kCtidPrefixMask) != kCtidPrefix)
         return std::nullopt;
 
-    uint32_t const ledgerSeq = static_cast((ctidValue >> 32) & 0x0FFF'FFFF);
-    uint16_t const txnIndex = static_cast((ctidValue >> 16) & 0xFFFF);
-    uint16_t const networkID = static_cast(ctidValue & 0xFFFF);
+    auto const ledgerSeq = static_cast((ctidValue >> 32) & 0x0FFF'FFFF);
+    auto const txnIndex = static_cast((ctidValue >> 16) & 0xFFFF);
+    auto const networkID = static_cast(ctidValue & 0xFFFF);
 
     return std::make_tuple(ledgerSeq, txnIndex, networkID);
 }
diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp
index 5d031c2c74..63dee76f1b 100644
--- a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp
+++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp
@@ -107,7 +107,7 @@ parseTakerIssuerJSON(
 
     if (taker.isMember(jss::currency))
     {
-        Issue& issue = asset.get();
+        auto& issue = asset.get();
 
         if (taker.isMember(jss::issuer))
         {

From f151293e8a87d192ed1e4f3c95d9782c602a2efb Mon Sep 17 00:00:00 2001
From: Ayaz Salikhov 
Date: Fri, 3 Jul 2026 12:17:03 +0100
Subject: [PATCH 044/100] chore: Enable modernize-avoid-bind (#7711)

---
 .clang-tidy                                   |  1 -
 include/xrpl/basics/TaggedCache.ipp           |  5 +-
 include/xrpl/beast/unit_test/thread.h         |  5 +-
 include/xrpl/net/AutoSocket.h                 |  9 +-
 include/xrpl/net/HTTPClientSSLContext.h       |  6 +-
 include/xrpl/server/detail/BaseHTTPPeer.h     | 74 +++++---------
 include/xrpl/server/detail/BasePeer.h         |  3 +-
 include/xrpl/server/detail/BaseWSPeer.h       | 66 ++++++-------
 include/xrpl/server/detail/Door.h             |  9 +-
 include/xrpl/server/detail/PlainHTTPPeer.h    | 10 +-
 include/xrpl/server/detail/SSLHTTPPeer.h      | 18 ++--
 src/libxrpl/basics/ResolverAsio.cpp           | 34 +++----
 src/libxrpl/beast/insight/StatsDCollector.cpp | 63 +++++-------
 src/libxrpl/core/detail/JobQueue.cpp          |  3 +-
 src/libxrpl/net/HTTPClient.cpp                | 69 +++++++------
 src/test/jtx/impl/WSClient.cpp                | 13 +--
 src/test/overlay/short_read_test.cpp          | 99 +++++++++----------
 src/test/resource/Logic_test.cpp              |  8 +-
 src/test/rpc/AccountTx_test.cpp               |  3 +-
 src/test/rpc/LedgerRequest_test.cpp           |  3 +-
 src/test/rpc/RPCCall_test.cpp                 |  3 +-
 src/test/rpc/TransactionEntry_test.cpp        |  3 +-
 src/test/rpc/Transaction_test.cpp             |  6 +-
 src/test/shamap/FetchPack_test.cpp            | 96 ++++++++----------
 src/xrpld/app/ledger/detail/LedgerMaster.cpp  |  2 +-
 src/xrpld/app/misc/NetworkOPs.cpp             |  2 +-
 src/xrpld/app/misc/SHAMapStoreImp.cpp         |  8 +-
 src/xrpld/app/misc/detail/WorkBase.h          | 38 +++----
 src/xrpld/app/misc/detail/WorkFile.h          |  6 +-
 src/xrpld/app/misc/detail/WorkSSL.cpp         |  3 +-
 .../app/rdb/backend/detail/SQLiteDatabase.cpp | 20 ++--
 src/xrpld/overlay/detail/ConnectAttempt.cpp   | 20 ++--
 src/xrpld/overlay/detail/OverlayImpl.cpp      | 10 +-
 src/xrpld/overlay/detail/OverlayImpl.h        |  2 +-
 src/xrpld/overlay/detail/PeerImp.cpp          | 28 +++---
 src/xrpld/peerfinder/detail/Checker.h         |  3 +-
 src/xrpld/peerfinder/detail/Logic.h           | 10 +-
 .../peerfinder/detail/PeerfinderManager.cpp   |  3 +-
 src/xrpld/rpc/detail/Pathfinder.cpp           |  9 +-
 src/xrpld/rpc/detail/RPCCall.cpp              | 28 +++---
 40 files changed, 356 insertions(+), 445 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index 6a24cbc8d3..c5f638b395 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -64,7 +64,6 @@ Checks: "-*,
   -misc-use-internal-linkage,
 
   modernize-*,
-  -modernize-avoid-bind,
   -modernize-avoid-c-arrays,
   -modernize-avoid-c-style-cast,
   -modernize-avoid-setjmp-longjmp,
diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp
index ffcd533216..ca50bb64b7 100644
--- a/include/xrpl/basics/TaggedCache.ipp
+++ b/include/xrpl/basics/TaggedCache.ipp
@@ -57,7 +57,10 @@ inline TaggedCache<
         beast::insight::Collector::ptr const& collector)
     : journal_(journal)
     , clock_(clock)
-    , stats_(name, std::bind(&TaggedCache::collectMetrics, this), collector)
+    , stats_(
+          name,
+          [this] { collectMetrics(); },
+          collector)
     , name_(name)
     , targetSize_(size)
     , targetAge_(expiration)
diff --git a/include/xrpl/beast/unit_test/thread.h b/include/xrpl/beast/unit_test/thread.h
index 0de039cb89..91d8cf3cab 100644
--- a/include/xrpl/beast/unit_test/thread.h
+++ b/include/xrpl/beast/unit_test/thread.h
@@ -45,7 +45,10 @@ public:
     template 
     explicit Thread(Suite& s, F&& f, Args&&... args) : s_(&s)
     {
-        std::function b = std::bind(std::forward(f), std::forward(args)...);
+        std::function b = [f = std::forward(f),
+                                       ... args = std::forward(args)]() mutable {
+            std::invoke(f, args...);
+        };
         t_ = std::thread(&Thread::run, this, std::move(b));
     }
 
diff --git a/include/xrpl/net/AutoSocket.h b/include/xrpl/net/AutoSocket.h
index 72eaed7439..b98885959d 100644
--- a/include/xrpl/net/AutoSocket.h
+++ b/include/xrpl/net/AutoSocket.h
@@ -120,12 +120,9 @@ public:
             socket_->next_layer().async_receive(
                 boost::asio::buffer(buffer_),
                 boost::asio::socket_base::message_peek,
-                std::bind(
-                    &AutoSocket::handleAutodetect,
-                    this,
-                    cbFunc,
-                    std::placeholders::_1,
-                    std::placeholders::_2));
+                [this, cbFunc](error_code const& ec, size_t bytesTransferred) {
+                    handleAutodetect(cbFunc, ec, bytesTransferred);
+                });
         }
     }
 
diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h
index 4192455ec9..2b26d7e404 100644
--- a/include/xrpl/net/HTTPClientSSLContext.h
+++ b/include/xrpl/net/HTTPClientSSLContext.h
@@ -13,7 +13,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -127,8 +126,9 @@ public:
             if (!ec)
             {
                 strm.set_verify_callback(
-                    std::bind(
-                        &rfc6125Verify, host, std::placeholders::_1, std::placeholders::_2, j_),
+                    [host, j = j_](bool preverified, boost::asio::ssl::verify_context& ctx) {
+                        return rfc6125Verify(host, preverified, ctx, j);
+                    },
                     ec);
             }
         }
diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h
index f096d3c5a9..7b35dbd4be 100644
--- a/include/xrpl/server/detail/BaseHTTPPeer.h
+++ b/include/xrpl/server/detail/BaseHTTPPeer.h
@@ -223,10 +223,7 @@ BaseHTTPPeer::close()
 {
     if (!strand_.running_in_this_thread())
     {
-        return post(
-            strand_,
-            std::bind(
-                (void (BaseHTTPPeer::*)(void))&BaseHTTPPeer::close, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->close(); });
     }
     boost::beast::get_lowest_layer(impl().stream_).close();
 }
@@ -322,22 +319,18 @@ BaseHTTPPeer::onWrite(error_code const& ec, std::size_t bytesTran
             v,
             bind_executor(
                 strand_,
-                std::bind(
-                    &BaseHTTPPeer::onWrite,
-                    impl().shared_from_this(),
-                    std::placeholders::_1,
-                    std::placeholders::_2)));
+                [self = impl().shared_from_this()](
+                    error_code const& ec, std::size_t bytesTransferred) {
+                    self->onWrite(ec, bytesTransferred);
+                }));
     }
     if (!complete_)
         return;
     if (graceful_)
         return doClose();
-    util::spawn(
-        strand_,
-        std::bind(
-            &BaseHTTPPeer::doRead,
-            impl().shared_from_this(),
-            std::placeholders::_1));
+    util::spawn(strand_, [self = impl().shared_from_this()](yield_context doYield) {
+        self->doRead(doYield);
+    });
 }
 
 template 
@@ -351,14 +344,9 @@ BaseHTTPPeer::doWriter(
     {
         auto const p = impl().shared_from_this();
         resume = std::function([this, p, writer, keepAlive]() {
-            util::spawn(
-                strand_,
-                std::bind(
-                    &BaseHTTPPeer::doWriter,
-                    p,
-                    writer,
-                    keepAlive,
-                    std::placeholders::_1));
+            util::spawn(strand_, [p, writer, keepAlive](yield_context doYield) {
+                p->doWriter(writer, keepAlive, doYield);
+            });
         });
     }
 
@@ -379,12 +367,9 @@ BaseHTTPPeer::doWriter(
     if (!keepAlive)
         return doClose();
 
-    util::spawn(
-        strand_,
-        std::bind(
-            &BaseHTTPPeer::doRead,
-            impl().shared_from_this(),
-            std::placeholders::_1));
+    util::spawn(strand_, [self = impl().shared_from_this()](yield_context doYield) {
+        self->doRead(doYield);
+    });
 }
 
 //------------------------------------------------------------------------------
@@ -405,8 +390,7 @@ BaseHTTPPeer::write(void const* buf, std::size_t bytes)
         if (!strand_.running_in_this_thread())
         {
             return post(
-                strand_,
-                std::bind(&BaseHTTPPeer::onWrite, impl().shared_from_this(), error_code{}, 0));
+                strand_, [self = impl().shared_from_this()] { self->onWrite(error_code{}, 0); });
         }
         return onWrite(error_code{}, 0);
     }
@@ -417,13 +401,9 @@ void
 BaseHTTPPeer::write(std::shared_ptr const& writer, bool keepAlive)
 {
     util::spawn(
-        strand_,
-        std::bind(
-            &BaseHTTPPeer::doWriter,
-            impl().shared_from_this(),
-            writer,
-            keepAlive,
-            std::placeholders::_1));
+        strand_, [self = impl().shared_from_this(), writer, keepAlive](yield_context doYield) {
+            self->doWriter(writer, keepAlive, doYield);
+        });
 }
 
 // DEPRECATED
@@ -443,8 +423,7 @@ BaseHTTPPeer::complete()
 {
     if (!strand_.running_in_this_thread())
     {
-        return post(
-            strand_, std::bind(&BaseHTTPPeer::complete, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->complete(); });
     }
 
     message_ = {};
@@ -457,12 +436,9 @@ BaseHTTPPeer::complete()
     }
 
     // keep-alive
-    util::spawn(
-        strand_,
-        std::bind(
-            &BaseHTTPPeer::doRead,
-            impl().shared_from_this(),
-            std::placeholders::_1));
+    util::spawn(strand_, [self = impl().shared_from_this()](yield_context doYield) {
+        self->doRead(doYield);
+    });
 }
 
 // DEPRECATED
@@ -474,11 +450,7 @@ BaseHTTPPeer::close(bool graceful)
     if (!strand_.running_in_this_thread())
     {
         return post(
-            strand_,
-            std::bind(
-                (void (BaseHTTPPeer::*)(bool))&BaseHTTPPeer::close,
-                impl().shared_from_this(),
-                graceful));
+            strand_, [self = impl().shared_from_this(), graceful] { self->close(graceful); });
     }
 
     complete_ = true;
diff --git a/include/xrpl/server/detail/BasePeer.h b/include/xrpl/server/detail/BasePeer.h
index 5301a2f018..84aa1e0da9 100644
--- a/include/xrpl/server/detail/BasePeer.h
+++ b/include/xrpl/server/detail/BasePeer.h
@@ -10,7 +10,6 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 
@@ -84,7 +83,7 @@ void
 BasePeer::close()
 {
     if (!strand_.running_in_this_thread())
-        return post(strand_, std::bind(&BasePeer::close, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->close(); });
     error_code ec;
     xrpl::getLowestLayer(impl().ws_).socket().close(ec);
 }
diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h
index 1b6a9faca3..d557140bd4 100644
--- a/include/xrpl/server/detail/BaseWSPeer.h
+++ b/include/xrpl/server/detail/BaseWSPeer.h
@@ -20,6 +20,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -180,12 +181,13 @@ void
 BaseWSPeer::run()
 {
     if (!strand_.running_in_this_thread())
-        return post(strand_, std::bind(&BaseWSPeer::run, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->run(); });
     impl().ws_.set_option(port().pmdOptions);
     // Must manage the control callback memory outside of the `control_callback`
     // function
-    controlCallback_ =
-        std::bind(&BaseWSPeer::onPingPong, this, std::placeholders::_1, std::placeholders::_2);
+    controlCallback_ = [this](
+                           boost::beast::websocket::frame_type kind,
+                           boost::beast::string_view payload) { onPingPong(kind, payload); };
     impl().ws_.control_callback(controlCallback_);
     startTimer();
     closeOnTimer_ = true;
@@ -193,11 +195,9 @@ BaseWSPeer::run()
         res.set(boost::beast::http::field::server, BuildInfo::getFullVersionString());
     }));
     impl().ws_.async_accept(
-        request_,
-        bind_executor(
-            strand_,
-            std::bind(
-                &BaseWSPeer::onWsHandshake, impl().shared_from_this(), std::placeholders::_1)));
+        request_, bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) {
+            self->onWsHandshake(ec);
+        }));
 }
 
 template 
@@ -205,7 +205,10 @@ void
 BaseWSPeer::send(std::shared_ptr w)
 {
     if (!strand_.running_in_this_thread())
-        return post(strand_, std::bind(&BaseWSPeer::send, impl().shared_from_this(), std::move(w)));
+    {
+        return post(
+            strand_, [self = impl().shared_from_this(), w = std::move(w)] { self->send(w); });
+    }
     if (doClose_)
         return;
     if (wq_.size() > port().wsQueueLimit)
@@ -258,7 +261,7 @@ void
 BaseWSPeer::complete()
 {
     if (!strand_.running_in_this_thread())
-        return post(strand_, std::bind(&BaseWSPeer::complete, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->complete(); });
     doRead();
 }
 
@@ -277,7 +280,7 @@ void
 BaseWSPeer::doWrite()
 {
     if (!strand_.running_in_this_thread())
-        return post(strand_, std::bind(&BaseWSPeer::doWrite, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->doWrite(); });
     onWrite({});
 }
 
@@ -288,8 +291,7 @@ BaseWSPeer::onWrite(error_code const& ec)
     if (ec)
         return fail(ec, "write");
     auto& w = *wq_.front();
-    auto const result =
-        w.prepare(65536, std::bind(&BaseWSPeer::doWrite, impl().shared_from_this()));
+    auto const result = w.prepare(65536, [self = impl().shared_from_this()] { self->doWrite(); });
     if (boost::indeterminate(result.first))
         return;
     startTimer();
@@ -299,8 +301,9 @@ BaseWSPeer::onWrite(error_code const& ec)
             static_cast(result.first),
             result.second,
             bind_executor(
-                strand_,
-                std::bind(&BaseWSPeer::onWrite, impl().shared_from_this(), std::placeholders::_1)));
+                strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) {
+                    self->onWrite(ec);
+                }));
     }
     else
     {
@@ -308,9 +311,9 @@ BaseWSPeer::onWrite(error_code const& ec)
             static_cast(result.first),
             result.second,
             bind_executor(
-                strand_,
-                std::bind(
-                    &BaseWSPeer::onWriteFin, impl().shared_from_this(), std::placeholders::_1)));
+                strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) {
+                    self->onWriteFin(ec);
+                }));
     }
 }
 
@@ -324,10 +327,9 @@ BaseWSPeer::onWriteFin(error_code const& ec)
     if (doClose_)
     {
         impl().ws_.async_close(
-            cr_,
-            bind_executor(
-                strand_,
-                std::bind(&BaseWSPeer::onClose, impl().shared_from_this(), std::placeholders::_1)));
+            cr_, bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) {
+                self->onClose(ec);
+            }));
     }
     else if (!wq_.empty())
     {
@@ -340,12 +342,13 @@ void
 BaseWSPeer::doRead()
 {
     if (!strand_.running_in_this_thread())
-        return post(strand_, std::bind(&BaseWSPeer::doRead, impl().shared_from_this()));
+        return post(strand_, [self = impl().shared_from_this()] { self->doRead(); });
     impl().ws_.async_read(
         rb_,
         bind_executor(
-            strand_,
-            std::bind(&BaseWSPeer::onRead, impl().shared_from_this(), std::placeholders::_1)));
+            strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) {
+                self->onRead(ec);
+            }));
 }
 
 template 
@@ -389,11 +392,7 @@ BaseWSPeer::startTimer()
     }
 
     timer_.async_wait(bind_executor(
-        strand_,
-        std::bind(
-            &BaseWSPeer::onTimer,
-            impl().shared_from_this(),
-            std::placeholders::_1)));
+        strand_, [self = impl().shared_from_this()](error_code const& ec) { self->onTimer(ec); }));
 }
 
 // Convenience for discarding the error code
@@ -461,10 +460,9 @@ BaseWSPeer::onTimer(error_code ec)
             beast::rngfill(payload_.begin(), payload_.size(), cryptoPrng());
             impl().ws_.async_ping(
                 payload_,
-                bind_executor(
-                    strand_,
-                    std::bind(
-                        &BaseWSPeer::onPing, impl().shared_from_this(), std::placeholders::_1)));
+                bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) {
+                    self->onPing(ec);
+                }));
             JLOG(this->j_.trace()) << "sent ping";
             return;
         }
diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h
index 09b0adc5f2..d2d7a7baf4 100644
--- a/include/xrpl/server/detail/Door.h
+++ b/include/xrpl/server/detail/Door.h
@@ -33,7 +33,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -182,7 +181,7 @@ void
 Door::Detector::run()
 {
     util::spawn(
-        strand_, std::bind(&Detector::doDetect, this->shared_from_this(), std::placeholders::_1));
+        strand_, [self = this->shared_from_this()](yield_context yield) { self->doDetect(yield); });
 }
 
 template 
@@ -297,8 +296,7 @@ void
 Door::run()
 {
     util::spawn(
-        strand_,
-        std::bind(&Door::doAccept, this->shared_from_this(), std::placeholders::_1));
+        strand_, [self = this->shared_from_this()](yield_context yield) { self->doAccept(yield); });
 }
 
 template 
@@ -307,8 +305,7 @@ Door::close()
 {
     if (!strand_.running_in_this_thread())
     {
-        return boost::asio::post(
-            strand_, std::bind(&Door::close, this->shared_from_this()));
+        return boost::asio::post(strand_, [self = this->shared_from_this()] { self->close(); });
     }
     backoffTimer_.cancel();
     error_code ec;
diff --git a/include/xrpl/server/detail/PlainHTTPPeer.h b/include/xrpl/server/detail/PlainHTTPPeer.h
index b89272fe9e..688fbbd425 100644
--- a/include/xrpl/server/detail/PlainHTTPPeer.h
+++ b/include/xrpl/server/detail/PlainHTTPPeer.h
@@ -9,7 +9,6 @@
 
 #include 
 
-#include 
 #include 
 #include 
 
@@ -89,7 +88,9 @@ PlainHTTPPeer::run()
 {
     if (!this->handler_.onAccept(this->session(), this->remoteAddress_))
     {
-        util::spawn(this->strand_, std::bind(&PlainHTTPPeer::doClose, this->shared_from_this()));
+        util::spawn(this->strand_, [self = this->shared_from_this()](boost::asio::yield_context) {
+            self->doClose();
+        });
         return;
     }
 
@@ -97,8 +98,9 @@ PlainHTTPPeer::run()
         return;
 
     util::spawn(
-        this->strand_,
-        std::bind(&PlainHTTPPeer::doRead, this->shared_from_this(), std::placeholders::_1));
+        this->strand_, [self = this->shared_from_this()](boost::asio::yield_context doYield) {
+            self->doRead(doYield);
+        });
 }
 
 template 
diff --git a/include/xrpl/server/detail/SSLHTTPPeer.h b/include/xrpl/server/detail/SSLHTTPPeer.h
index ea2108b917..7fe21ed6b5 100644
--- a/include/xrpl/server/detail/SSLHTTPPeer.h
+++ b/include/xrpl/server/detail/SSLHTTPPeer.h
@@ -13,7 +13,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 
@@ -99,14 +98,15 @@ SSLHTTPPeer::run()
 {
     if (!this->handler_.onAccept(this->session(), this->remoteAddress_))
     {
-        util::spawn(this->strand_, std::bind(&SSLHTTPPeer::doClose, this->shared_from_this()));
+        util::spawn(
+            this->strand_, [self = this->shared_from_this()](yield_context) { self->doClose(); });
         return;
     }
     if (!socket_.is_open())
         return;
-    util::spawn(
-        this->strand_,
-        std::bind(&SSLHTTPPeer::doHandshake, this->shared_from_this(), std::placeholders::_1));
+    util::spawn(this->strand_, [self = this->shared_from_this()](yield_context doYield) {
+        self->doHandshake(doYield);
+    });
 }
 
 template 
@@ -142,9 +142,9 @@ SSLHTTPPeer::doHandshake(yield_context doYield)
         this->port().protocol.count("https") > 0;
     if (http)
     {
-        util::spawn(
-            this->strand_,
-            std::bind(&SSLHTTPPeer::doRead, this->shared_from_this(), std::placeholders::_1));
+        util::spawn(this->strand_, [self = this->shared_from_this()](yield_context doYield) {
+            self->doRead(doYield);
+        });
         return;
     }
     // `this` will be destroyed
@@ -172,7 +172,7 @@ SSLHTTPPeer::doClose()
     this->startTimer();
     stream_.async_shutdown(bind_executor(
         this->strand_,
-        std::bind(&SSLHTTPPeer::onShutdown, this->shared_from_this(), std::placeholders::_1)));
+        [self = this->shared_from_this()](error_code const& ec) { self->onShutdown(ec); }));
 }
 
 template 
diff --git a/src/libxrpl/basics/ResolverAsio.cpp b/src/libxrpl/basics/ResolverAsio.cpp
index ddf006b681..7e1b56f87a 100644
--- a/src/libxrpl/basics/ResolverAsio.cpp
+++ b/src/libxrpl/basics/ResolverAsio.cpp
@@ -192,7 +192,7 @@ public:
             boost::asio::dispatch(
                 ioContext,
                 boost::asio::bind_executor(
-                    strand, std::bind(&ResolverAsioImpl::doStop, this, CompletionCounter(this))));
+                    strand, [this, counter = CompletionCounter(this)] { doStop(counter); }));
 
             JLOG(journal.debug()) << "Queued a stop request";
         }
@@ -221,9 +221,9 @@ public:
         boost::asio::dispatch(
             ioContext,
             boost::asio::bind_executor(
-                strand,
-                std::bind(
-                    &ResolverAsioImpl::doResolve, this, names, handler, CompletionCounter(this))));
+                strand, [this, names, handler, counter = CompletionCounter(this)] {
+                    doResolve(names, handler, counter);
+                }));
     }
 
     //-------------------------------------------------------------------------
@@ -272,7 +272,7 @@ public:
         boost::asio::post(
             ioContext,
             boost::asio::bind_executor(
-                strand, std::bind(&ResolverAsioImpl::doWork, this, CompletionCounter(this))));
+                strand, [this, counter = CompletionCounter(this)] { doWork(counter); }));
     }
 
     static HostAndPort
@@ -291,8 +291,10 @@ public:
         // a port separator
 
         // Attempt to find the first and last non-whitespace
-        auto const findWhitespace =
-            std::bind(&std::isspace, std::placeholders::_1, std::locale());
+        std::locale const loc;
+        auto const findWhitespace = [&loc](std::string::value_type c) {
+            return std::isspace(c, loc);
+        };
 
         auto hostFirst = std::ranges::find_if_not(str, findWhitespace);
 
@@ -348,7 +350,7 @@ public:
             boost::asio::post(
                 ioContext,
                 boost::asio::bind_executor(
-                    strand, std::bind(&ResolverAsioImpl::doWork, this, CompletionCounter(this))));
+                    strand, [this, counter = CompletionCounter(this)] { doWork(counter); }));
 
             return;
         }
@@ -356,14 +358,11 @@ public:
         resolver.async_resolve(
             host,
             port,
-            std::bind(
-                &ResolverAsioImpl::doFinish,
-                this,
-                name,
-                std::placeholders::_1,
-                handler,
-                std::placeholders::_2,
-                CompletionCounter(this)));
+            [this, name, handler, counter = CompletionCounter(this)](
+                boost::system::error_code const& ec,
+                boost::asio::ip::tcp::resolver::results_type results) {
+                doFinish(name, ec, handler, results, counter);
+            });
     }
 
     void
@@ -383,8 +382,7 @@ public:
                 boost::asio::post(
                     ioContext,
                     boost::asio::bind_executor(
-                        strand,
-                        std::bind(&ResolverAsioImpl::doWork, this, CompletionCounter(this))));
+                        strand, [this, counter = CompletionCounter(this)] { doWork(counter); }));
             }
         }
     }
diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp
index 1da5315de1..3cff5d93b5 100644
--- a/src/libxrpl/beast/insight/StatsDCollector.cpp
+++ b/src/libxrpl/beast/insight/StatsDCollector.cpp
@@ -325,9 +325,9 @@ public:
     postBuffer(std::string&& buffer)
     {
         boost::asio::dispatch(
-            ioContext_,
-            boost::asio::bind_executor(
-                strand_, std::bind(&StatsDCollectorImp::doPostBuffer, this, std::move(buffer))));
+            ioContext_, boost::asio::bind_executor(strand_, [this, buffer = std::move(buffer)] {
+                doPostBuffer(buffer);
+            }));
     }
 
     // The keepAlive parameter makes sure the buffers sent to
@@ -392,12 +392,10 @@ public:
                 log(buffers);
                 socket_.async_send(
                     buffers,
-                    std::bind(
-                        &StatsDCollectorImp::onSend,
-                        this,
-                        keepAlive,
-                        std::placeholders::_1,
-                        std::placeholders::_2));
+                    [this, keepAlive](
+                        boost::system::error_code const& ec, std::size_t bytesTransferred) {
+                        onSend(keepAlive, ec, bytesTransferred);
+                    });
                 buffers.clear();
                 size = 0;
             }
@@ -411,12 +409,10 @@ public:
             log(buffers);
             socket_.async_send(
                 buffers,
-                std::bind(
-                    &StatsDCollectorImp::onSend,
-                    this,
-                    keepAlive,
-                    std::placeholders::_1,
-                    std::placeholders::_2));
+                [this, keepAlive](
+                    boost::system::error_code const& ec, std::size_t bytesTransferred) {
+                    onSend(keepAlive, ec, bytesTransferred);
+                });
         }
     }
 
@@ -425,7 +421,7 @@ public:
     {
         using namespace std::chrono_literals;
         timer_.expires_after(1s);
-        timer_.async_wait(std::bind(&StatsDCollectorImp::onTimer, this, std::placeholders::_1));
+        timer_.async_wait([this](boost::system::error_code const& ec) { onTimer(ec); });
     }
 
     void
@@ -513,10 +509,9 @@ StatsDCounterImpl::increment(CounterImpl::value_type amount)
 {
     boost::asio::dispatch(
         impl_->getIoContext(),
-        std::bind(
-            &StatsDCounterImpl::doIncrement,
-            std::static_pointer_cast(shared_from_this()),
-            amount));
+        [self = std::static_pointer_cast(shared_from_this()), amount] {
+            self->doIncrement(amount);
+        });
 }
 
 void
@@ -558,10 +553,9 @@ StatsDEventImpl::notify(EventImpl::value_type const& value)
 {
     boost::asio::dispatch(
         impl_->getIoContext(),
-        std::bind(
-            &StatsDEventImpl::doNotify,
-            std::static_pointer_cast(shared_from_this()),
-            value));
+        [self = std::static_pointer_cast(shared_from_this()), value] {
+            self->doNotify(value);
+        });
 }
 
 void
@@ -591,10 +585,9 @@ StatsDGaugeImpl::set(GaugeImpl::value_type value)
 {
     boost::asio::dispatch(
         impl_->getIoContext(),
-        std::bind(
-            &StatsDGaugeImpl::doSet,
-            std::static_pointer_cast(shared_from_this()),
-            value));
+        [self = std::static_pointer_cast(shared_from_this()), value] {
+            self->doSet(value);
+        });
 }
 
 void
@@ -602,10 +595,9 @@ StatsDGaugeImpl::increment(GaugeImpl::difference_type amount)
 {
     boost::asio::dispatch(
         impl_->getIoContext(),
-        std::bind(
-            &StatsDGaugeImpl::doIncrement,
-            std::static_pointer_cast(shared_from_this()),
-            amount));
+        [self = std::static_pointer_cast(shared_from_this()), amount] {
+            self->doIncrement(amount);
+        });
 }
 
 void
@@ -678,10 +670,9 @@ StatsDMeterImpl::increment(MeterImpl::value_type amount)
 {
     boost::asio::dispatch(
         impl_->getIoContext(),
-        std::bind(
-            &StatsDMeterImpl::doIncrement,
-            std::static_pointer_cast(shared_from_this()),
-            amount));
+        [self = std::static_pointer_cast(shared_from_this()), amount] {
+            self->doIncrement(amount);
+        });
 }
 
 void
diff --git a/src/libxrpl/core/detail/JobQueue.cpp b/src/libxrpl/core/detail/JobQueue.cpp
index f773270af3..8f95a8a5fa 100644
--- a/src/libxrpl/core/detail/JobQueue.cpp
+++ b/src/libxrpl/core/detail/JobQueue.cpp
@@ -13,7 +13,6 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -36,7 +35,7 @@ JobQueue::JobQueue(
 {
     JLOG(journal_.info()) << "Using " << threadCount << "  threads";
 
-    hook_ = collector_->makeHook(std::bind(&JobQueue::collect, this));
+    hook_ = collector_->makeHook([this] { collect(); });
     jobCount_ = collector_->makeGauge("job_count");
 
     {
diff --git a/src/libxrpl/net/HTTPClient.cpp b/src/libxrpl/net/HTTPClient.cpp
index 4b9cc9d6e6..7294633964 100644
--- a/src/libxrpl/net/HTTPClient.cpp
+++ b/src/libxrpl/net/HTTPClient.cpp
@@ -134,12 +134,10 @@ public:
         request(
             bSSL,
             deqSites,
-            std::bind(
-                &HTTPClientImp::makeGet,
-                shared_from_this(),
-                strPath,
-                std::placeholders::_1,
-                std::placeholders::_2),
+            [self = shared_from_this(), strPath](
+                boost::asio::streambuf& sb, std::string const& strHost) {
+                self->makeGet(strPath, sb, strHost);
+            },
             timeout,
             complete);
     }
@@ -166,9 +164,9 @@ public:
             shutdown_ = e.code();
 
             JLOG(j_.trace()) << "expires_after: " << shutdown_.message();
-            deadline_.async_wait(
-                std::bind(
-                    &HTTPClientImp::handleDeadline, shared_from_this(), std::placeholders::_1));
+            deadline_.async_wait([self = shared_from_this()](boost::system::error_code const& ec) {
+                self->handleDeadline(ec);
+            });
         }
 
         if (!shutdown_)
@@ -179,11 +177,11 @@ public:
                 query_->host,
                 query_->port,
                 query_->flags,
-                std::bind(
-                    &HTTPClientImp::handleResolve,
-                    shared_from_this(),
-                    std::placeholders::_1,
-                    std::placeholders::_2));
+                [self = shared_from_this()](
+                    boost::system::error_code const& ecResult,
+                    boost::asio::ip::tcp::resolver::results_type results) {
+                    self->handleResolve(ecResult, results);
+                });
         }
 
         if (shutdown_)
@@ -220,9 +218,9 @@ public:
             resolver_.cancel();
 
             // Stop the transaction.
-            socket_.asyncShutdown(
-                std::bind(
-                    &HTTPClientImp::handleShutdown, shared_from_this(), std::placeholders::_1));
+            socket_.asyncShutdown([self = shared_from_this()](boost::system::error_code const& ec) {
+                self->handleShutdown(ec);
+            });
         }
     }
 
@@ -262,8 +260,9 @@ public:
             boost::asio::async_connect(
                 socket_.lowestLayer(),
                 result,
-                std::bind(
-                    &HTTPClientImp::handleConnect, shared_from_this(), std::placeholders::_1));
+                [self = shared_from_this()](
+                    boost::system::error_code const& ecResult,
+                    boost::asio::ip::tcp::endpoint const&) { self->handleConnect(ecResult); });
         }
     }
 
@@ -301,8 +300,9 @@ public:
         {
             socket_.asyncHandshake(
                 AutoSocket::ssl_socket::client,
-                std::bind(
-                    &HTTPClientImp::handleRequest, shared_from_this(), std::placeholders::_1));
+                [self = shared_from_this()](boost::system::error_code const& ec) {
+                    self->handleRequest(ec);
+                });
         }
         else
         {
@@ -330,11 +330,10 @@ public:
 
             socket_.asyncWrite(
                 request_,
-                std::bind(
-                    &HTTPClientImp::handleWrite,
-                    shared_from_this(),
-                    std::placeholders::_1,
-                    std::placeholders::_2));
+                [self = shared_from_this()](
+                    boost::system::error_code const& ecResult, std::size_t bytesTransferred) {
+                    self->handleWrite(ecResult, bytesTransferred);
+                });
         }
     }
 
@@ -357,11 +356,10 @@ public:
             socket_.asyncReadUntil(
                 header_,
                 "\r\n\r\n",
-                std::bind(
-                    &HTTPClientImp::handleHeader,
-                    shared_from_this(),
-                    std::placeholders::_1,
-                    std::placeholders::_2));
+                [self = shared_from_this()](
+                    boost::system::error_code const& ecResult, std::size_t bytesTransferred) {
+                    self->handleHeader(ecResult, bytesTransferred);
+                });
         }
     }
 
@@ -424,11 +422,10 @@ public:
             socket_.asyncRead(
                 response_.prepare(responseSize - body_.size()),
                 boost::asio::transfer_all(),
-                std::bind(
-                    &HTTPClientImp::handleData,
-                    shared_from_this(),
-                    std::placeholders::_1,
-                    std::placeholders::_2));
+                [self = shared_from_this()](
+                    boost::system::error_code const& ecResult, std::size_t bytesTransferred) {
+                    self->handleData(ecResult, bytesTransferred);
+                });
         }
     }
 
diff --git a/src/test/jtx/impl/WSClient.cpp b/src/test/jtx/impl/WSClient.cpp
index c00a88f270..ca322415fb 100644
--- a/src/test/jtx/impl/WSClient.cpp
+++ b/src/test/jtx/impl/WSClient.cpp
@@ -30,6 +30,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -172,9 +173,9 @@ public:
                     }));
             ws_.handshake(ep.address().to_string() + ":" + std::to_string(ep.port()), "/");
             ws_.async_read(
-                rb_,
-                boost::asio::bind_executor(
-                    strand_, std::bind(&WSClientImpl::onReadMsg, this, std::placeholders::_1)));
+                rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) {
+                    onReadMsg(ec);
+                }));
         }
         catch (std::exception&)
         {
@@ -310,9 +311,9 @@ private:
             cv_.notify_all();
         }
         ws_.async_read(
-            rb_,
-            boost::asio::bind_executor(
-                strand_, std::bind(&WSClientImpl::onReadMsg, this, std::placeholders::_1)));
+            rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) {
+                onReadMsg(ec);
+            }));
     }
 
     // Called when the read op terminates
diff --git a/src/test/overlay/short_read_test.cpp b/src/test/overlay/short_read_test.cpp
index 9af632d65c..f235008d89 100644
--- a/src/test/overlay/short_read_test.cpp
+++ b/src/test/overlay/short_read_test.cpp
@@ -26,7 +26,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -199,7 +198,7 @@ private:
             {
                 if (!strand.running_in_this_thread())
                 {
-                    post(strand, std::bind(&Acceptor::close, shared_from_this()));
+                    post(strand, [self = shared_from_this()] { self->close(); });
                     return;
                 }
                 acceptor.close();
@@ -210,9 +209,9 @@ private:
             {
                 acceptor.async_accept(
                     socket,
-                    bind_executor(
-                        strand,
-                        std::bind(&Acceptor::onAccept, shared_from_this(), std::placeholders::_1)));
+                    bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
+                        self->onAccept(ec);
+                    }));
             }
 
             void
@@ -239,9 +238,9 @@ private:
                 p->run();
                 acceptor.async_accept(
                     socket,
-                    bind_executor(
-                        strand,
-                        std::bind(&Acceptor::onAccept, shared_from_this(), std::placeholders::_1)));
+                    bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
+                        self->onAccept(ec);
+                    }));
             }
         };
 
@@ -271,7 +270,7 @@ private:
             {
                 if (!strand.running_in_this_thread())
                 {
-                    post(strand, std::bind(&Connection::close, shared_from_this()));
+                    post(strand, [self = shared_from_this()] { self->close(); });
                     return;
                 }
                 if (socket.is_open())
@@ -287,13 +286,12 @@ private:
                 timer.expires_after(std::chrono::seconds(3));
                 timer.async_wait(bind_executor(
                     strand,
-                    std::bind(&Connection::onTimer, shared_from_this(), std::placeholders::_1)));
+                    [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); }));
                 stream.async_handshake(
                     stream_type::server,
-                    bind_executor(
-                        strand,
-                        std::bind(
-                            &Connection::onHandshake, shared_from_this(), std::placeholders::_1)));
+                    bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
+                        self->onHandshake(ec);
+                    }));
             }
 
             void
@@ -337,11 +335,10 @@ private:
                     "\n",
                     bind_executor(
                         strand,
-                        std::bind(
-                            &Connection::onRead,
-                            shared_from_this(),
-                            std::placeholders::_1,
-                            std::placeholders::_2)));
+                        [self = shared_from_this()](
+                            error_code const& ec, std::size_t bytesTransferred) {
+                            self->onRead(ec, bytesTransferred);
+                        }));
 #else
                 close();
 #endif
@@ -353,10 +350,10 @@ private:
                 if (ec == boost::asio::error::eof)
                 {
                     server.test_.log << "[server] read: EOF" << std::endl;
-                    stream.async_shutdown(bind_executor(
-                        strand,
-                        std::bind(
-                            &Connection::onShutdown, shared_from_this(), std::placeholders::_1)));
+                    stream.async_shutdown(
+                        bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
+                            self->onShutdown(ec);
+                        }));
                     return;
                 }
                 if (ec)
@@ -373,11 +370,10 @@ private:
                     buf.data(),
                     bind_executor(
                         strand,
-                        std::bind(
-                            &Connection::onWrite,
-                            shared_from_this(),
-                            std::placeholders::_1,
-                            std::placeholders::_2)));
+                        [self = shared_from_this()](
+                            error_code const& ec, std::size_t bytesTransferred) {
+                            self->onWrite(ec, bytesTransferred);
+                        }));
             }
 
             void
@@ -391,7 +387,7 @@ private:
                 }
                 stream.async_shutdown(bind_executor(
                     strand,
-                    std::bind(&Connection::onShutdown, shared_from_this(), std::placeholders::_1)));
+                    [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); }));
             }
 
             void
@@ -463,7 +459,7 @@ private:
             {
                 if (!strand.running_in_this_thread())
                 {
-                    post(strand, std::bind(&Connection::close, shared_from_this()));
+                    post(strand, [self = shared_from_this()] { self->close(); });
                     return;
                 }
                 if (socket.is_open())
@@ -479,13 +475,11 @@ private:
                 timer.expires_after(std::chrono::seconds(3));
                 timer.async_wait(bind_executor(
                     strand,
-                    std::bind(&Connection::onTimer, shared_from_this(), std::placeholders::_1)));
+                    [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); }));
                 socket.async_connect(
-                    ep,
-                    bind_executor(
-                        strand,
-                        std::bind(
-                            &Connection::onConnect, shared_from_this(), std::placeholders::_1)));
+                    ep, bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
+                        self->onConnect(ec);
+                    }));
             }
 
             void
@@ -524,10 +518,9 @@ private:
                 }
                 stream.async_handshake(
                     stream_type::client,
-                    bind_executor(
-                        strand,
-                        std::bind(
-                            &Connection::onHandshake, shared_from_this(), std::placeholders::_1)));
+                    bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
+                        self->onHandshake(ec);
+                    }));
             }
 
             void
@@ -546,16 +539,14 @@ private:
                     buf.data(),
                     bind_executor(
                         strand,
-                        std::bind(
-                            &Connection::onWrite,
-                            shared_from_this(),
-                            std::placeholders::_1,
-                            std::placeholders::_2)));
+                        [self = shared_from_this()](
+                            error_code const& ec, std::size_t bytesTransferred) {
+                            self->onWrite(ec, bytesTransferred);
+                        }));
 #else
                 stream_.async_shutdown(bind_executor(
                     strand_,
-                    std::bind(
-                        &Connection::on_shutdown, shared_from_this(), std::placeholders::_1)));
+                    [self = shared_from_this()](error_code const& ec) { self->on_shutdown(ec); }));
 #endif
             }
 
@@ -575,16 +566,14 @@ private:
                     "\n",
                     bind_executor(
                         strand,
-                        std::bind(
-                            &Connection::onRead,
-                            shared_from_this(),
-                            std::placeholders::_1,
-                            std::placeholders::_2)));
+                        [self = shared_from_this()](
+                            error_code const& ec, std::size_t bytesTransferred) {
+                            self->onRead(ec, bytesTransferred);
+                        }));
 #else
                 stream_.async_shutdown(bind_executor(
                     strand_,
-                    std::bind(
-                        &Connection::on_shutdown, shared_from_this(), std::placeholders::_1)));
+                    [self = shared_from_this()](error_code const& ec) { self->on_shutdown(ec); }));
 #endif
             }
 
@@ -599,7 +588,7 @@ private:
                 buf.commit(bytesTransferred);
                 stream.async_shutdown(bind_executor(
                     strand,
-                    std::bind(&Connection::onShutdown, shared_from_this(), std::placeholders::_1)));
+                    [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); }));
             }
 
             void
diff --git a/src/test/resource/Logic_test.cpp b/src/test/resource/Logic_test.cpp
index de4575cb16..f6b4875356 100644
--- a/src/test/resource/Logic_test.cpp
+++ b/src/test/resource/Logic_test.cpp
@@ -89,9 +89,11 @@ public:
         Charge const fee(kDropThreshold + 1);
         beast::IP::Endpoint const addr(beast::IP::Endpoint::fromString("192.0.2.2"));
 
-        std::function const ep = limited
-            ? std::bind(&TestLogic::newInboundEndpoint, &logic, std::placeholders::_1)
-            : std::bind(&TestLogic::newUnlimitedEndpoint, &logic, std::placeholders::_1);
+        std::function const ep =
+            [&logic, limited](beast::IP::Endpoint const& address) {
+                return limited ? logic.newInboundEndpoint(address)
+                               : logic.newUnlimitedEndpoint(address);
+            };
 
         {
             Consumer c(ep(addr));
diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp
index bd7b07e936..ace8743e1e 100644
--- a/src/test/rpc/AccountTx_test.cpp
+++ b/src/test/rpc/AccountTx_test.cpp
@@ -41,7 +41,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -893,7 +892,7 @@ public:
     void
     run() override
     {
-        forAllApiVersions(std::bind_front(&AccountTx_test::testParameters, this));
+        forAllApiVersions([this](unsigned apiVersion) { testParameters(apiVersion); });
         testContents();
         testAccountDelete();
         testMPT();
diff --git a/src/test/rpc/LedgerRequest_test.cpp b/src/test/rpc/LedgerRequest_test.cpp
index 4759256b96..93feee9497 100644
--- a/src/test/rpc/LedgerRequest_test.cpp
+++ b/src/test/rpc/LedgerRequest_test.cpp
@@ -11,7 +11,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -350,7 +349,7 @@ public:
     {
         testLedgerRequest();
         testEvolution();
-        forAllApiVersions(std::bind_front(&LedgerRequest_test::testBadInput, this));
+        forAllApiVersions([this](unsigned apiVersion) { testBadInput(apiVersion); });
         testMoreThan256Closed();
         testNonAdmin();
     }
diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp
index cd3cd3cb41..4b5ab1f230 100644
--- a/src/test/rpc/RPCCall_test.cpp
+++ b/src/test/rpc/RPCCall_test.cpp
@@ -14,7 +14,6 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -5927,7 +5926,7 @@ public:
     void
     run() override
     {
-        forAllApiVersions(std::bind_front(&RPCCall_test::testRPCCall, this));
+        forAllApiVersions([this](unsigned apiVersion) { testRPCCall(apiVersion); });
     }
 };
 
diff --git a/src/test/rpc/TransactionEntry_test.cpp b/src/test/rpc/TransactionEntry_test.cpp
index 8724e2974d..38a95e84f8 100644
--- a/src/test/rpc/TransactionEntry_test.cpp
+++ b/src/test/rpc/TransactionEntry_test.cpp
@@ -17,7 +17,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -353,7 +352,7 @@ public:
     run() override
     {
         testBadInput();
-        forAllApiVersions(std::bind_front(&TransactionEntry_test::testRequest, this));
+        forAllApiVersions([this](unsigned apiVersion) { testRequest(apiVersion); });
     }
 };
 
diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp
index 8c50736b85..c3bf707ec4 100644
--- a/src/test/rpc/Transaction_test.cpp
+++ b/src/test/rpc/Transaction_test.cpp
@@ -31,7 +31,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -886,7 +885,7 @@ public:
     run() override
     {
         using namespace test::jtx;
-        forAllApiVersions(std::bind_front(&Transaction_test::testBinaryRequest, this));
+        forAllApiVersions([this](unsigned apiVersion) { testBinaryRequest(apiVersion); });
 
         FeatureBitset const all{testableAmendments()};
         testWithFeats(all);
@@ -899,7 +898,8 @@ public:
         testRangeCTIDRequest(features);
         testCTIDValidation(features);
         testRPCsForCTID(features);
-        forAllApiVersions(std::bind_front(&Transaction_test::testRequest, this, features));
+        forAllApiVersions(
+            [this, features](unsigned apiVersion) { testRequest(features, apiVersion); });
     }
 };
 
diff --git a/src/test/shamap/FetchPack_test.cpp b/src/test/shamap/FetchPack_test.cpp
index bcc5129f01..f2e7578ee2 100644
--- a/src/test/shamap/FetchPack_test.cpp
+++ b/src/test/shamap/FetchPack_test.cpp
@@ -6,7 +6,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -26,7 +25,6 @@
 #include 
 #include 
 #include 
-#include 
 
 namespace xrpl::tests {
 
@@ -40,15 +38,6 @@ public:
     using Table = SHAMap;
     using Item = SHAMapItem;
 
-    struct Handler
-    {
-        void
-        operator()(std::uint32_t refNum) const
-        {
-            Throw("missing node");
-        }
-    };
-
     struct TestFilter : SHAMapSyncFilter
     {
         TestFilter(Map& map, beast::Journal journal) : map(map), journal(journal)
@@ -93,7 +82,7 @@ public:
     static void
     addRandomItems(std::size_t n, Table& t, beast::xor_shift_engine& r)
     {
-        while ((n--) != 0u)
+        for (std::size_t i = 0; i < n; ++i)
         {
             auto const result(t.addItem(SHAMapNodeType::TnAccountState, makeRandomItemMember(r)));
             assert(result);
@@ -111,56 +100,51 @@ public:
     void
     run() override
     {
-        using beast::Severity;
+        testFetchPack();
+    }
+
+    // Exercises a fetch-pack round trip: build a SHAMap, serialize every node
+    // into a pack keyed by node hash, then rebuild the map in a fresh SHAMap by
+    // sourcing every node from the pack through a SHAMapSyncFilter and comparing
+    // the result. This covers the filter-based reconstruction path (fetchRoot +
+    // getMissingNodes with a SHAMapSyncFilter), complementing SHAMapSync_test,
+    // which drives the getNodeFat/addKnownNode path.
+    void
+    testFetchPack()
+    {
         test::SuiteJournal journal("FetchPack_test", *this);
+        TestNodeFamily f(journal), f2(journal);
+        beast::xor_shift_engine r;
 
-        TestNodeFamily f(journal);
-        std::shared_ptr const t1(std::make_shared
(SHAMapType::FREE, f)); + // Build a source map. getHash() unshares the tree and computes every + // node hash; this must happen before serializing nodes below, otherwise + // inner nodes still carry stale cached hashes. + auto const source = std::make_shared
(SHAMapType::FREE, f); + addRandomItems(kTableItems + kTableItemsExtra, *source, r); + source->setImmutable(); + auto const rootHash = source->getHash(); - pass(); + // Turn the source into a fetch pack: node hash -> serialized node. + Map map; + source->visitNodes([this, &map](SHAMapTreeNode& node) { + Serializer s; + node.serializeWithPrefix(s); + onFetch(map, node.getHash(), s.getData()); + return true; + }); - // beast::Random r; - // add_random_items_ (tableItems, *t1, r); - // std::shared_ptr
t2 (t1->snapShot (true)); - // - // add_random_items_ (tableItemsExtra, *t1, r); - // add_random_items_ (tableItemsExtra, *t2, r); + // Rebuild the map in a fresh family, sourcing every node from the pack + // through the SHAMapSyncFilter. + auto const rebuilt = std::make_shared
(SHAMapType::FREE, rootHash.asUInt256(), f2); + TestFilter filter(map, journal); + rebuilt->setSynching(); + BEAST_EXPECT(rebuilt->fetchRoot(rootHash, &filter)); - // turn t1 into t2 - // Map map; - // t2->getFetchPack (t1.get(), true, 1000000, std::bind ( - // &FetchPack_test::on_fetch, this, std::ref (map), - // std::placeholders::_1, std::placeholders::_2)); - // t1->getFetchPack (nullptr, true, 1000000, std::bind ( - // &FetchPack_test::on_fetch, this, std::ref (map), - // std::placeholders::_1, std::placeholders::_2)); + // Everything should be in the pack, so no nodes should be missing. + BEAST_EXPECT(rebuilt->getMissingNodes(2048, &filter).empty()); + rebuilt->clearSynching(); - // try to rebuild t2 from the fetch pack - // std::shared_ptr
t3; - // try - // { - // TestFilter filter (map, beast::Journal()); - // - // t3 = std::make_shared
(SHAMapType::FREE, - // t2->getHash (), - // fullBelowCache); - // - // BEAST_EXPECT(t3->fetchRoot (t2->getHash (), &filter), - // "unable to get root"); - // - // // everything should be in the pack, no hashes should be - // needed std::vector hashes = - // t3->getNeededHashes(1, &filter); - // BEAST_EXPECT(hashes.empty(), "missing hashes"); - // - // BEAST_EXPECT(t3->getHash () == t2->getHash (), "root - // hashes do not match"); BEAST_EXPECT(t3->deepCompare - // (*t2), "failed compare"); - // } - // catch (std::exception const&) - // { - // fail ("unhandled exception"); - // } + BEAST_EXPECT(rebuilt->deepCompare(*source)); } }; diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 3fcac03ed5..bab0dca827 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -137,7 +137,7 @@ LedgerMaster::LedgerMaster( std::chrono::seconds{45}, stopwatch, app_.getJournal("TaggedCache")) - , stats_(std::bind(&LedgerMaster::collectMetrics, this), collector) + , stats_([this] { collectMetrics(); }, collector) { } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index d1f794b74d..4d40247a29 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -330,7 +330,7 @@ public: , jobQueue_(jobQueue) , standalone_(standalone) , minPeerCount_(startValid ? 0 : minPeerCount) - , stats_(std::bind(&NetworkOPsImp::collectMetrics, this), collector) + , stats_([this] { collectMetrics(); }, collector) { } diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 0389130f7a..0e0099cdbf 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -331,11 +331,9 @@ SHAMapStoreImp::run() try { validatedLedger->stateMap().snapShot(false)->visitNodes( - std::bind( - &SHAMapStoreImp::copyNode, - this, - std::ref(nodeCount), - std::placeholders::_1)); + [this, &nodeCount](SHAMapTreeNode const& node) { + return copyNode(nodeCount, node); + }); } catch (SHAMapMissingNode const& e) { diff --git a/src/xrpld/app/misc/detail/WorkBase.h b/src/xrpld/app/misc/detail/WorkBase.h index 5f5fdd28b7..73e5081036 100644 --- a/src/xrpld/app/misc/detail/WorkBase.h +++ b/src/xrpld/app/misc/detail/WorkBase.h @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -138,9 +139,9 @@ WorkBase::run() if (!strand_.running_in_this_thread()) { return boost::asio::post( - ios_, - boost::asio::bind_executor( - strand_, std::bind(&WorkBase::run, impl().shared_from_this()))); + ios_, boost::asio::bind_executor(strand_, [self = impl().shared_from_this()] { + self->run(); + })); } resolver_.async_resolve( @@ -148,11 +149,9 @@ WorkBase::run() port_, boost::asio::bind_executor( strand_, - std::bind( - &WorkBase::onResolve, - impl().shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = impl().shared_from_this()](error_code const& ec, results_type results) { + self->onResolve(ec, results); + })); } template @@ -165,7 +164,7 @@ WorkBase::cancel() ios_, boost::asio::bind_executor( - strand_, std::bind(&WorkBase::cancel, impl().shared_from_this()))); + strand_, [self = impl().shared_from_this()] { self->cancel(); })); } error_code ec; @@ -196,11 +195,12 @@ WorkBase::onResolve(error_code const& ec, results_type results) results, boost::asio::bind_executor( strand_, - std::bind( - &WorkBase::onConnect, - impl().shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = impl().shared_from_this()]( + error_code const& ec, endpoint_type const& endpoint) { + // Call the base-class overload explicitly: the derived Impl + // hides it with its own single-argument onConnect(ec). + self->WorkBase::onConnect(ec, endpoint); + })); } template @@ -229,8 +229,9 @@ WorkBase::onStart() impl().stream(), req_, boost::asio::bind_executor( - strand_, - std::bind(&WorkBase::onRequest, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onRequest(ec); + })); } template @@ -245,8 +246,9 @@ WorkBase::onRequest(error_code const& ec) readBuf_, res_, boost::asio::bind_executor( - strand_, - std::bind(&WorkBase::onResponse, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onResponse(ec); + })); } template diff --git a/src/xrpld/app/misc/detail/WorkFile.h b/src/xrpld/app/misc/detail/WorkFile.h index 2fc7ab952d..eb42d14db4 100644 --- a/src/xrpld/app/misc/detail/WorkFile.h +++ b/src/xrpld/app/misc/detail/WorkFile.h @@ -63,9 +63,9 @@ WorkFile::run() { if (!strand_.running_in_this_thread()) { - boost::asio::post( - ios_, - boost::asio::bind_executor(strand_, std::bind(&WorkFile::run, shared_from_this()))); + boost::asio::post(ios_, boost::asio::bind_executor(strand_, [self = shared_from_this()] { + self->run(); + })); return; } diff --git a/src/xrpld/app/misc/detail/WorkSSL.cpp b/src/xrpld/app/misc/detail/WorkSSL.cpp index e1b864e81f..e8d24b55d6 100644 --- a/src/xrpld/app/misc/detail/WorkSSL.cpp +++ b/src/xrpld/app/misc/detail/WorkSSL.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include @@ -55,7 +54,7 @@ WorkSSL::onConnect(error_code const& ec) stream_.async_handshake( boost::asio::ssl::stream_base::client, boost::asio::bind_executor( - strand_, std::bind(&WorkSSL::onHandshake, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onHandshake(ec); })); } void diff --git a/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp index fd1298516f..338a3a33df 100644 --- a/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp +++ b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp @@ -421,8 +421,9 @@ SQLiteDatabase::oldestAccountTxPage(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(200); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; AccountTxs ret; auto onTransaction = [&ret, &app = registry_.get().getApp()]( std::uint32_t ledgerIndex, @@ -451,8 +452,9 @@ SQLiteDatabase::newestAccountTxPage(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(200); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; AccountTxs ret; auto onTransaction = [&ret, &app = registry_.get().getApp()]( std::uint32_t ledgerIndex, @@ -481,8 +483,9 @@ SQLiteDatabase::oldestAccountTxPageB(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(500); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; MetaTxsList ret; auto onTransaction = [&ret]( @@ -509,8 +512,9 @@ SQLiteDatabase::newestAccountTxPageB(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(500); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; MetaTxsList ret; auto onTransaction = [&ret]( diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 295f4f5497..064b4ecd3e 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -35,8 +35,8 @@ #include #include +#include #include -#include #include #include #include @@ -86,7 +86,7 @@ ConnectAttempt::stop() { if (!strand_.running_in_this_thread()) { - boost::asio::post(strand_, std::bind(&ConnectAttempt::stop, shared_from_this())); + boost::asio::post(strand_, [self = shared_from_this()] { self->stop(); }); return; } if (socket_.is_open()) @@ -104,8 +104,7 @@ ConnectAttempt::run() stream_.next_layer().async_connect( remoteEndpoint_, boost::asio::bind_executor( - strand_, - std::bind(&ConnectAttempt::onConnect, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onConnect(ec); })); } //------------------------------------------------------------------------------ @@ -160,8 +159,7 @@ ConnectAttempt::setTimer() timer_.async_wait( boost::asio::bind_executor( - strand_, - std::bind(&ConnectAttempt::onTimer, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } void @@ -228,8 +226,7 @@ ConnectAttempt::onConnect(error_code ec) stream_.async_handshake( boost::asio::ssl::stream_base::client, boost::asio::bind_executor( - strand_, - std::bind(&ConnectAttempt::onHandshake, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onHandshake(ec); })); } void @@ -290,7 +287,7 @@ ConnectAttempt::onHandshake(error_code ec) req_, boost::asio::bind_executor( strand_, - std::bind(&ConnectAttempt::onWrite, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec, std::size_t) { self->onWrite(ec); })); } void @@ -316,7 +313,7 @@ ConnectAttempt::onWrite(error_code ec) response_, boost::asio::bind_executor( strand_, - std::bind(&ConnectAttempt::onRead, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec, std::size_t) { self->onRead(ec); })); } void @@ -339,8 +336,7 @@ ConnectAttempt::onRead(error_code ec) stream_.async_shutdown( boost::asio::bind_executor( strand_, - std::bind( - &ConnectAttempt::onShutdown, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); return; } diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 89c7dfe5eb..b7af3b6ace 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -133,7 +133,7 @@ OverlayImpl::Timer::asyncWait() timer.async_wait( boost::asio::bind_executor( overlay_.strand_, - std::bind(&Timer::onTimer, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } void @@ -190,7 +190,7 @@ OverlayImpl::OverlayImpl( , nextId_(1) , slots_(app, *this, app.config()) , stats_( - std::bind(&OverlayImpl::collectMetrics, this), + [this] { collectMetrics(); }, collector, [counts = traffic_.getCounts(), collector]() { std::unordered_map ret; @@ -593,7 +593,7 @@ OverlayImpl::start() void OverlayImpl::stop() { - boost::asio::dispatch(strand_, std::bind(&OverlayImpl::stopChildren, this)); + boost::asio::dispatch(strand_, [this] { stopChildren(); }); { std::unique_lock lock(mutex_); cond_.wait(lock, [this] { return list_.empty(); }); @@ -1490,7 +1490,7 @@ OverlayImpl::deletePeer(Peer::id_t id) { if (!strand_.running_in_this_thread()) { - post(strand_, std::bind(&OverlayImpl::deletePeer, this, id)); + post(strand_, [this, id] { deletePeer(id); }); return; } @@ -1502,7 +1502,7 @@ OverlayImpl::deleteIdlePeers() { if (!strand_.running_in_this_thread()) { - post(strand_, std::bind(&OverlayImpl::deleteIdlePeers, this)); + post(strand_, [this] { deleteIdlePeers(); }); return; } diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index 7db32c0d23..83d5a81a89 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -427,7 +427,7 @@ public: addTxMetrics(Args... args) { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&OverlayImpl::addTxMetrics, this, args...)); + return post(strand_, [this, args...] { addTxMetrics(args...); }); txMetrics_.addMetrics(args...); } diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 83e4d9c851..4e42d46f46 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -321,9 +321,9 @@ PeerImp::send(std::shared_ptr const& m) self->stream_, boost::asio::buffer(self->sendQueue_.front()->getBuffer(self->compressionEnabled_)), bind_executor( - self->strand_, - std::bind( - &PeerImp::onWriteMessage, self, std::placeholders::_1, std::placeholders::_2))); + self->strand_, [self](error_code const& ec, std::size_t bytesTransferred) { + self->onWriteMessage(ec, bytesTransferred); + })); }); } @@ -650,7 +650,7 @@ PeerImp::gracefulClose() return; setTimer(); stream_.async_shutdown(bind_executor( - strand_, std::bind(&PeerImp::onShutdown, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); } void @@ -666,7 +666,7 @@ PeerImp::setTimer() return; } timer_.async_wait(bind_executor( - strand_, std::bind(&PeerImp::onTimer, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } // convenience for ignoring the error code @@ -975,11 +975,9 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytesTransferred) readBuffer_.prepare(std::max(Tuning::kReadBufferBytes, hint)), bind_executor( strand_, - std::bind( - &PeerImp::onReadMessage, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()](error_code const& ec, std::size_t bytesTransferred) { + self->onReadMessage(ec, bytesTransferred); + })); } void @@ -1014,18 +1012,16 @@ PeerImp::onWriteMessage(error_code ec, std::size_t bytesTransferred) boost::asio::buffer(sendQueue_.front()->getBuffer(compressionEnabled_)), bind_executor( strand_, - std::bind( - &PeerImp::onWriteMessage, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()](error_code const& ec, std::size_t bytesTransferred) { + self->onWriteMessage(ec, bytesTransferred); + })); return; } if (gracefulClose_) { stream_.async_shutdown(bind_executor( - strand_, std::bind(&PeerImp::onShutdown, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); return; } } diff --git a/src/xrpld/peerfinder/detail/Checker.h b/src/xrpld/peerfinder/detail/Checker.h index 20687448df..208bad390c 100644 --- a/src/xrpld/peerfinder/detail/Checker.h +++ b/src/xrpld/peerfinder/detail/Checker.h @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -183,7 +182,7 @@ Checker::asyncConnect(beast::IP::Endpoint const& endpoint, Handler&& h } op->socket.async_connect( beast::IPAddressConversion::toAsioEndpoint(endpoint), - std::bind(&BasicAsyncOp::operator(), op, std::placeholders::_1)); + [op](error_code const& ec) { (*op)(ec); }); } template diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 7d10f7f516..8ea348f560 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -802,12 +802,10 @@ public: // checker.asyncConnect( ep.address, - std::bind( - &Logic::checkComplete, - this, - slot->remoteEndpoint(), - ep.address, - std::placeholders::_1)); + [this, remoteAddress = slot->remoteEndpoint(), checkedAddress = ep.address]( + boost::system::error_code const& ec) { + checkComplete(remoteAddress, checkedAddress, ec); + }); // Note that we simply discard the first Endpoint // that the neighbor sends when we perform the diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp index 9dbedfe4f1..2727a03013 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include @@ -61,7 +60,7 @@ public: , checker_(io_context_) , logic_(clock, store_, checker_, journal) , config_(config) - , stats_(std::bind(&ManagerImp::collectMetrics, this), collector) + , stats_([this] { collectMetrics(); }, collector) { } diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index fc788dea7d..5b7f1415a2 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -1158,11 +1158,10 @@ Pathfinder::addLink( { std::ranges::sort( candidates, - std::bind( - compareAccountCandidate, - ledger_->seq(), - std::placeholders::_1, - std::placeholders::_2)); + [seq = ledger_->seq()]( + AccountCandidate const& first, AccountCandidate const& second) { + return compareAccountCandidate(seq, first, second); + }); int count = candidates.size(); // allow more paths from source diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index 123b9fc7a5..af5677008d 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -1745,7 +1745,9 @@ rpcClient( static_cast(setup.client.secure) != 0, // Use SSL config.quiet(), logs, - std::bind(RPCCallImp::callRPCHandler, &jvOutput, std::placeholders::_1), + [&jvOutput](json::Value const& jvInput) { + RPCCallImp::callRPCHandler(&jvOutput, jvInput); + }, headers); isService.run(); // This blocks until there are no more // outstanding async calls. @@ -1870,24 +1872,16 @@ fromNetwork( ioContext, strIp, iPort, - std::bind( - &RPCCallImp::onRequest, - strMethod, - jvParams, - headers, - strPath, - std::placeholders::_1, - std::placeholders::_2, - j), + [strMethod, jvParams, headers, strPath, j]( + boost::asio::streambuf& sb, std::string const& strHost) { + RPCCallImp::onRequest(strMethod, jvParams, headers, strPath, sb, strHost, j); + }, kRpcReplyMaxBytes, kRpcWebhookTimeout, - std::bind( - &RPCCallImp::onResponse, - callbackFuncP, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - j), + [callbackFuncP, j]( + boost::system::error_code const& ecResult, int iStatus, std::string const& strData) { + return RPCCallImp::onResponse(callbackFuncP, ecResult, iStatus, strData, j); + }, j); } From 53649cc298c5a7c23aa75cb09b66b62431a28c69 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 3 Jul 2026 15:28:15 +0100 Subject: [PATCH 045/100] chore: Enable modernize-use-constraints (#7715) --- .clang-tidy | 1 - include/xrpl/basics/Slice.h | 6 +- include/xrpl/basics/TaggedCache.h | 6 +- include/xrpl/basics/TaggedCache.ipp | 6 +- include/xrpl/basics/ToString.h | 3 +- include/xrpl/basics/base_uint.h | 26 ++- include/xrpl/basics/random.h | 29 ++- include/xrpl/basics/safe_cast.h | 18 +- include/xrpl/basics/scope.h | 25 +-- include/xrpl/basics/tagged_integer.h | 8 +- .../beast/container/aged_container_utility.h | 4 +- .../detail/aged_container_iterator.h | 16 +- .../container/detail/aged_ordered_container.h | 189 +++++++++--------- .../detail/aged_unordered_container.h | 158 ++++++++------- include/xrpl/beast/core/LexicalCast.h | 9 +- include/xrpl/beast/hash/hash_append.h | 93 +++++---- include/xrpl/beast/hash/xxhasher.h | 12 +- include/xrpl/beast/utility/rngfill.h | 7 +- include/xrpl/core/JobQueue.h | 8 +- .../xrpl/ledger/helpers/DirectoryHelpers.h | 14 +- include/xrpl/net/HTTPClientSSLContext.h | 18 +- include/xrpl/nodestore/detail/varint.h | 9 +- include/xrpl/protocol/STArray.h | 28 +-- include/xrpl/protocol/STObject.h | 16 +- include/xrpl/protocol/TER.h | 59 +++--- include/xrpl/server/Manifest.h | 5 +- src/libxrpl/protocol/STParsedJSON.cpp | 6 +- src/libxrpl/protocol/STTx.cpp | 2 +- src/libxrpl/protocol/tokens.cpp | 3 +- src/test/app/NFToken_test.cpp | 2 +- .../beast/aged_associative_container_test.cpp | 119 +++++++---- src/test/csf/Tx.h | 6 +- src/test/csf/submitters.h | 3 +- src/test/jtx/TestHelpers.h | 3 +- src/test/jtx/amount.h | 22 +- src/test/jtx/paths.h | 3 +- src/test/protocol/Quality_test.cpp | 6 +- src/test/protocol/TER_test.cpp | 9 +- src/xrpld/overlay/detail/PeerImp.h | 12 +- src/xrpld/overlay/detail/ProtocolMessage.h | 13 +- src/xrpld/rpc/Status.h | 8 +- 41 files changed, 549 insertions(+), 441 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index c5f638b395..3bad686a11 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -80,7 +80,6 @@ Checks: "-*, -modernize-return-braced-init-list, -modernize-shrink-to-fit, -modernize-use-bool-literals, - -modernize-use-constraints, -modernize-use-default-member-init, -modernize-use-integer-sign-comparison, -modernize-use-noexcept, diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 948d012958..f87ca063b8 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -211,15 +211,17 @@ operator<<(Stream& s, Slice const& v) } template -std::enable_if_t || std::is_same_v, Slice> +Slice makeSlice(std::array const& a) + requires(std::is_same_v || std::is_same_v) { return Slice(a.data(), a.size()); } template -std::enable_if_t || std::is_same_v, Slice> +Slice makeSlice(std::vector const& v) + requires(std::is_same_v || std::is_same_v) { return Slice(v.data(), v.size()); } diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h index 71ebfc57a4..16c87cf833 100644 --- a/include/xrpl/basics/TaggedCache.h +++ b/include/xrpl/basics/TaggedCache.h @@ -212,11 +212,13 @@ public: */ template auto - insert(key_type const& key, T const& value) -> std::enable_if_t; + insert(key_type const& key, T const& value) -> ReturnType + requires(!IsKeyCache); template auto - insert(key_type const& key) -> std::enable_if_t; + insert(key_type const& key) -> ReturnType + requires IsKeyCache; // VFALCO NOTE It looks like this returns a copy of the data in // the output parameter 'data'. This could be expensive. diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index ca50bb64b7..b79561c71a 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -503,7 +503,8 @@ template < template inline auto TaggedCache:: - insert(key_type const& key, T const& value) -> std::enable_if_t + insert(key_type const& key, T const& value) -> ReturnType + requires(!IsKeyCache) { static_assert( std::is_same_v, SharedPointerType> || @@ -533,7 +534,8 @@ template < template inline auto TaggedCache:: - insert(key_type const& key) -> std::enable_if_t + insert(key_type const& key) -> ReturnType + requires IsKeyCache { std::scoped_lock const lock(mutex_); clock_type::time_point const now(clock_.now()); diff --git a/include/xrpl/basics/ToString.h b/include/xrpl/basics/ToString.h index 7764c1e3e3..e9f8f43633 100644 --- a/include/xrpl/basics/ToString.h +++ b/include/xrpl/basics/ToString.h @@ -12,8 +12,9 @@ namespace xrpl { */ template -std::enable_if_t, std::string> +std::string to_string(T t) // NOLINT(readability-identifier-naming) + requires(std::is_arithmetic_v) { return std::to_string(t); } diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index c60fbf35b4..481a7dbd77 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -280,12 +280,11 @@ public: { } - template < - class Container, - class = std::enable_if_t< - detail::IsContiguousContainer::value && - std::is_trivially_copyable_v>> + template explicit BaseUInt(Container const& c) + requires( + detail::IsContiguousContainer::value && + std::is_trivially_copyable_v) { // Use AlwaysFalseT so the static_assert condition is dependent // and only triggers when this constructor template is instantiated. @@ -295,13 +294,12 @@ public: "Use base_uint::fromRaw instead."); } - template < - class Container, - class = std::enable_if_t< - detail::IsContiguousContainer::value && - std::is_trivially_copyable_v>> + template static BaseUInt fromRaw(Container const& c) + requires( + detail::IsContiguousContainer::value && + std::is_trivially_copyable_v) { BaseUInt result; XRPL_ASSERT( @@ -312,11 +310,11 @@ public: } template - std::enable_if_t< - detail::IsContiguousContainer::value && - std::is_trivially_copyable_v, - BaseUInt&> + BaseUInt& operator=(Container const& c) + requires( + detail::IsContiguousContainer::value && + std::is_trivially_copyable_v) { XRPL_ASSERT( c.size() * sizeof(typename Container::value_type) == size(), diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index cceaa6f029..c544e7d0c8 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -91,8 +91,9 @@ defaultPrng() */ /** @{ */ template -std::enable_if_t && detail::is_engine::value, Integral> +Integral randInt(Engine& engine, Integral min, Integral max) + requires(std::is_integral_v && detail::is_engine::value) { XRPL_ASSERT(max > min, "xrpl::randInt : max over min inputs"); @@ -103,36 +104,41 @@ randInt(Engine& engine, Integral min, Integral max) } template -std::enable_if_t, Integral> +Integral randInt(Integral min, Integral max) + requires(std::is_integral_v) { return randInt(defaultPrng(), min, max); } template -std::enable_if_t && detail::is_engine::value, Integral> +Integral randInt(Engine& engine, Integral max) + requires(std::is_integral_v && detail::is_engine::value) { return randInt(engine, Integral(0), max); } template -std::enable_if_t, Integral> +Integral randInt(Integral max) + requires(std::is_integral_v) { return randInt(defaultPrng(), max); } template -std::enable_if_t && detail::is_engine::value, Integral> +Integral randInt(Engine& engine) + requires(std::is_integral_v && detail::is_engine::value) { return randInt(engine, std::numeric_limits::max()); } template -std::enable_if_t, Integral> +Integral randInt() + requires(std::is_integral_v) { return randInt(defaultPrng(), std::numeric_limits::max()); } @@ -141,19 +147,20 @@ randInt() /** Return a random byte */ /** @{ */ template -std::enable_if_t< - (std::is_same_v || std::is_same_v) && - detail::is_engine::value, - Byte> +Byte randByte(Engine& engine) + requires( + (std::is_same_v || std::is_same_v) && + detail::is_engine::value) { return static_cast(randInt( engine, std::numeric_limits::min(), std::numeric_limits::max())); } template -std::enable_if_t<(std::is_same_v || std::is_same_v), Byte> +Byte randByte() + requires(std::is_same_v || std::is_same_v) { return randByte(defaultPrng()); } diff --git a/include/xrpl/basics/safe_cast.h b/include/xrpl/basics/safe_cast.h index 714146e089..483a783f5d 100644 --- a/include/xrpl/basics/safe_cast.h +++ b/include/xrpl/basics/safe_cast.h @@ -15,8 +15,9 @@ concept SafeToCast = (std::is_integral_v && std::is_integral_v) && : sizeof(Dest) >= sizeof(Src)); template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest safeCast(Src s) noexcept + requires(std::is_integral_v && std::is_integral_v) { static_assert( std::is_signed_v || std::is_unsigned_v, "Cannot cast signed to unsigned"); @@ -28,15 +29,17 @@ safeCast(Src s) noexcept } template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest safeCast(Src s) noexcept + requires(std::is_enum_v && std::is_integral_v) { return static_cast(safeCast>(s)); } template -constexpr std::enable_if_t && std::is_enum_v, Dest> +constexpr Dest safeCast(Src s) noexcept + requires(std::is_integral_v && std::is_enum_v) { return safeCast(static_cast>(s)); } @@ -46,8 +49,9 @@ safeCast(Src s) noexcept // underlying types become safe, it can be converted to a safe_cast. template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest unsafeCast(Src s) noexcept + requires(std::is_integral_v && std::is_integral_v) { static_assert( !SafeToCast, @@ -57,15 +61,17 @@ unsafeCast(Src s) noexcept } template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest unsafeCast(Src s) noexcept + requires(std::is_enum_v && std::is_integral_v) { return static_cast(unsafeCast>(s)); } template -constexpr std::enable_if_t && std::is_enum_v, Dest> +constexpr Dest unsafeCast(Src s) noexcept + requires(std::is_integral_v && std::is_enum_v) { return unsafeCast(static_cast>(s)); } diff --git a/include/xrpl/basics/scope.h b/include/xrpl/basics/scope.h index cfd21e6e30..e63bb69eb5 100644 --- a/include/xrpl/basics/scope.h +++ b/include/xrpl/basics/scope.h @@ -46,11 +46,9 @@ public: operator=(ScopeExit&&) = delete; template - explicit ScopeExit( - EFP&& f, - std::enable_if_t< - !std::is_same_v, ScopeExit> && - std::is_constructible_v>* = 0) noexcept + explicit ScopeExit(EFP&& f) noexcept + requires( + !std::is_same_v, ScopeExit> && std::is_constructible_v) : exitFunction_{std::forward(f)} { static_assert(std::is_nothrow_constructible_v(f))>); @@ -93,11 +91,9 @@ public: operator=(ScopeFail&&) = delete; template - explicit ScopeFail( - EFP&& f, - std::enable_if_t< - !std::is_same_v, ScopeFail> && - std::is_constructible_v>* = 0) noexcept + explicit ScopeFail(EFP&& f) noexcept + requires( + !std::is_same_v, ScopeFail> && std::is_constructible_v) : exitFunction_{std::forward(f)} { static_assert(std::is_nothrow_constructible_v(f))>); @@ -140,12 +136,11 @@ public: operator=(ScopeSuccess&&) = delete; template - explicit ScopeSuccess( - EFP&& f, - std::enable_if_t< + explicit ScopeSuccess(EFP&& f) noexcept( + std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) + requires( !std::is_same_v, ScopeSuccess> && - std::is_constructible_v>* = - 0) noexcept(std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) + std::is_constructible_v) : exitFunction_{std::forward(f)} { } diff --git a/include/xrpl/basics/tagged_integer.h b/include/xrpl/basics/tagged_integer.h index 8fbd2a274b..5a088db863 100644 --- a/include/xrpl/basics/tagged_integer.h +++ b/include/xrpl/basics/tagged_integer.h @@ -43,10 +43,10 @@ public: TaggedInteger() = default; - template < - class OtherInt, - class = std::enable_if_t && sizeof(OtherInt) <= sizeof(Int)>> - explicit constexpr TaggedInteger(OtherInt value) noexcept : value_(value) + template + explicit constexpr TaggedInteger(OtherInt value) noexcept + requires(std::is_integral_v && sizeof(OtherInt) <= sizeof(Int)) + : value_(value) { static_assert(sizeof(TaggedInteger) == sizeof(Int), "tagged_integer is adding padding"); } diff --git a/include/xrpl/beast/container/aged_container_utility.h b/include/xrpl/beast/container/aged_container_utility.h index 7cda863fab..f43e59b0f7 100644 --- a/include/xrpl/beast/container/aged_container_utility.h +++ b/include/xrpl/beast/container/aged_container_utility.h @@ -4,14 +4,14 @@ #include #include -#include namespace beast { /** Expire aged container items past the specified age. */ template -std::enable_if_t::value, std::size_t> +std::size_t expire(AgedContainer& c, std::chrono::duration const& age) + requires(IsAgedContainer::value) { std::size_t n(0); auto const expired(c.clock().now() - age); diff --git a/include/xrpl/beast/container/detail/aged_container_iterator.h b/include/xrpl/beast/container/detail/aged_container_iterator.h index 02fb3927dd..d6c061bb86 100644 --- a/include/xrpl/beast/container/detail/aged_container_iterator.h +++ b/include/xrpl/beast/container/detail/aged_container_iterator.h @@ -30,20 +30,19 @@ public: // Disable constructing a const_iterator from a non-const_iterator. // Converting between reverse and non-reverse iterators should be explicit. - template < - bool OtherIsConst, - class OtherIterator, - class = std::enable_if_t< - (!OtherIsConst || IsConst) && - !static_cast(std::is_same_v)>> + template explicit AgedContainerIterator(AgedContainerIterator const& other) + requires( + (!OtherIsConst || IsConst) && + !static_cast(std::is_same_v)) : iter_(other.iter_) { } // Disable constructing a const_iterator from a non-const_iterator. - template > + template AgedContainerIterator(AgedContainerIterator const& other) + requires(!OtherIsConst || IsConst) : iter_(other.iter_) { } @@ -52,7 +51,8 @@ public: template auto operator=(AgedContainerIterator const& other) - -> std::enable_if_t + -> AgedContainerIterator& + requires(!OtherIsConst || IsConst) { iter_ = other.iter_; return *this; diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 0533a51f00..f739f05d2c 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -111,10 +111,9 @@ private: { } - template < - class... Args, - class = std::enable_if_t>> + template Element(time_point const& when, Args&&... args) + requires(std::is_constructible_v) : value(std::forward(args)...), when(when) { } @@ -608,35 +607,25 @@ public: // //-------------------------------------------------------------------------- - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - at(K const& k); + at(K const& k) + requires(MaybeMap && !MaybeMulti); - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional::type const& - at(K const& k) const; + at(K const& k) const + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key const& key); + operator[](Key const& key) + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key&& key); + operator[](Key&& key) + requires(MaybeMap && !MaybeMulti); //-------------------------------------------------------------------------- // @@ -770,35 +759,40 @@ public: // map, set template auto - insert(value_type const& value) -> std::enable_if_t>; + insert(value_type const& value) -> std::pair + requires(!MaybeMulti); // multimap, multiset template auto - insert(value_type const& value) -> std::enable_if_t; + insert(value_type const& value) -> iterator + requires MaybeMulti; // set template auto - insert(value_type&& value) - -> std::enable_if_t>; + insert(value_type&& value) -> std::pair + requires(!MaybeMulti && !MaybeMap); // multiset template auto - insert(value_type&& value) -> std::enable_if_t; + insert(value_type&& value) -> iterator + requires(MaybeMulti && !MaybeMap); //--- // map, set template auto - insert(const_iterator hint, value_type const& value) -> std::enable_if_t; + insert(const_iterator hint, value_type const& value) -> iterator + requires(!MaybeMulti); // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type const& value) + requires MaybeMulti { // VFALCO TODO Figure out how to utilize 'hint' return insert(value); @@ -807,12 +801,14 @@ public: // map, set template auto - insert(const_iterator hint, value_type&& value) -> std::enable_if_t; + insert(const_iterator hint, value_type&& value) -> iterator + requires(!MaybeMulti); // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type&& value) + requires MaybeMulti { // VFALCO TODO Figure out how to utilize 'hint' return insert(std::move(value)); @@ -820,20 +816,18 @@ public: // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplace(std::forward

(value)); } // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(const_iterator hint, P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplaceHint(hint, std::forward

(value)); } @@ -855,46 +849,45 @@ public: // map, set template auto - emplace(Args&&... args) -> std::enable_if_t>; + emplace(Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template auto - emplace(Args&&... args) -> std::enable_if_t; + emplace(Args&&... args) -> iterator + requires MaybeMulti; // map, set template auto - emplaceHint(const_iterator hint, Args&&... args) - -> std::enable_if_t>; + emplaceHint(const_iterator hint, Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template - std::enable_if_t + iterator emplaceHint(const_iterator /*hint*/, Args&&... args) + requires MaybeMulti { // VFALCO TODO Figure out how to utilize 'hint' return emplace(std::forward(args)...); } - // enable_if prevents erase (reverse_iterator pos) from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + // The constraint prevents erase (reverse_iterator pos) from compiling + template beast::detail::AgedContainerIterator - erase(beast::detail::AgedContainerIterator pos); + erase(beast::detail::AgedContainerIterator pos) + requires(!IsBoostReverseIterator::value); - // enable_if prevents erase (reverse_iterator first, reverse_iterator last) + // The constraint prevents erase (reverse_iterator first, reverse_iterator last) // from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + template beast::detail::AgedContainerIterator erase( beast::detail::AgedContainerIterator first, - beast::detail::AgedContainerIterator last); + beast::detail::AgedContainerIterator last) + requires(!IsBoostReverseIterator::value); template auto @@ -905,13 +898,11 @@ public: //-------------------------------------------------------------------------- - // enable_if prevents touch (reverse_iterator pos) from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + // The constraint prevents touch (reverse_iterator pos) from compiling + template void touch(beast::detail::AgedContainerIterator pos) + requires(!IsBoostReverseIterator::value) { touch(pos, clock().now()); } @@ -1142,25 +1133,25 @@ public: } private: - // enable_if prevents erase (reverse_iterator pos, now) from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + // The constraint prevents erase (reverse_iterator pos, now) from compiling + template void touch( beast::detail::AgedContainerIterator pos, - clock_type::time_point const& now); + clock_type::time_point const& now) + requires(!IsBoostReverseIterator::value); template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t - swapData(AgedOrderedContainer& other) noexcept; + void + swapData(AgedOrderedContainer& other) noexcept + requires MaybePropagate; template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t - swapData(AgedOrderedContainer& other) noexcept; + void + swapData(AgedOrderedContainer& other) noexcept + requires(!MaybePropagate); private: ConfigT config_; @@ -1369,9 +1360,10 @@ AgedOrderedContainer::operato //------------------------------------------------------------------------------ template -template +template std::conditional_t& AgedOrderedContainer::at(K const& k) + requires(MaybeMap && !MaybeMulti) { auto const iter(cont_.find(k, std::cref(config_.keyCompare()))); if (iter == cont_.end()) @@ -1380,9 +1372,10 @@ AgedOrderedContainer::at(K co } template -template +template std::conditional::type const& AgedOrderedContainer::at(K const& k) const + requires(MaybeMap && !MaybeMulti) { auto const iter(cont_.find(k, std::cref(config_.keyCompare()))); if (iter == cont_.end()) @@ -1391,9 +1384,10 @@ AgedOrderedContainer::at(K co } template -template +template std::conditional_t& AgedOrderedContainer::operator[](Key const& key) + requires(MaybeMap && !MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(key, std::cref(config_.keyCompare()), d)); @@ -1409,9 +1403,10 @@ AgedOrderedContainer::operato } template -template +template std::conditional_t& AgedOrderedContainer::operator[](Key&& key) + requires(MaybeMap && !MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(key, std::cref(config_.keyCompare()), d)); @@ -1445,7 +1440,8 @@ template auto AgedOrderedContainer::insert( - value_type const& value) -> std::enable_if_t> + value_type const& value) -> std::pair + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(extract(value), std::cref(config_.keyCompare()), d)); @@ -1464,7 +1460,8 @@ template auto AgedOrderedContainer::insert( - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires MaybeMulti { auto const before(cont_.upper_bound(extract(value), std::cref(config_.keyCompare()))); Element* const p(newElement(value)); @@ -1478,7 +1475,8 @@ template auto AgedOrderedContainer::insert(value_type&& value) - -> std::enable_if_t> + -> std::pair + requires(!MaybeMulti && !MaybeMap) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(extract(value), std::cref(config_.keyCompare()), d)); @@ -1497,7 +1495,8 @@ template auto AgedOrderedContainer::insert(value_type&& value) - -> std::enable_if_t + -> iterator + requires(MaybeMulti && !MaybeMap) { auto const before(cont_.upper_bound(extract(value), std::cref(config_.keyCompare()))); Element* const p(newElement(std::move(value))); @@ -1514,7 +1513,8 @@ template auto AgedOrderedContainer::insert( const_iterator hint, - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result( @@ -1535,7 +1535,8 @@ template auto AgedOrderedContainer::insert( const_iterator hint, - value_type&& value) -> std::enable_if_t + value_type&& value) -> iterator + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result( @@ -1555,7 +1556,8 @@ template auto AgedOrderedContainer::emplace(Args&&... args) - -> std::enable_if_t> + -> std::pair + requires(!MaybeMulti) { // VFALCO NOTE Its unfortunate that we need to // construct element here @@ -1577,7 +1579,8 @@ template auto AgedOrderedContainer::emplace(Args&&... args) - -> std::enable_if_t + -> iterator + requires MaybeMulti { Element* const p(newElement(std::forward(args)...)); auto const before(cont_.upper_bound(extract(p->value), std::cref(config_.keyCompare()))); @@ -1592,7 +1595,8 @@ template auto AgedOrderedContainer::emplaceHint( const_iterator hint, - Args&&... args) -> std::enable_if_t> + Args&&... args) -> std::pair + requires(!MaybeMulti) { // VFALCO NOTE Its unfortunate that we need to // construct element here @@ -1611,21 +1615,23 @@ AgedOrderedContainer::emplace } template -template +template beast::detail::AgedContainerIterator AgedOrderedContainer::erase( beast::detail::AgedContainerIterator pos) + requires(!IsBoostReverseIterator::value) { unlinkAndDeleteElement(&*((pos++).iterator())); return beast::detail::AgedContainerIterator(pos.iterator()); } template -template +template beast::detail::AgedContainerIterator AgedOrderedContainer::erase( beast::detail::AgedContainerIterator first, beast::detail::AgedContainerIterator last) + requires(!IsBoostReverseIterator::value) { for (; first != last;) unlinkAndDeleteElement(&*((first++).iterator())); @@ -1728,11 +1734,12 @@ AgedOrderedContainer::operato //------------------------------------------------------------------------------ template -template +template void AgedOrderedContainer::touch( beast::detail::AgedContainerIterator pos, clock_type::time_point const& now) + requires(!IsBoostReverseIterator::value) { auto& e(*pos.iterator()); e.when = now; @@ -1742,9 +1749,10 @@ AgedOrderedContainer::touch( template template -std::enable_if_t +void AgedOrderedContainer::swapData( AgedOrderedContainer& other) noexcept + requires MaybePropagate { std::swap(config_.keyCompare(), other.config_.keyCompare()); std::swap(config_.alloc(), other.config_.alloc()); @@ -1753,9 +1761,10 @@ AgedOrderedContainer::swapDat template template -std::enable_if_t +void AgedOrderedContainer::swapData( AgedOrderedContainer& other) noexcept + requires(!MaybePropagate) { std::swap(config_.keyCompare(), other.config_.keyCompare()); std::swap(config_.clock, other.config_.clock); diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index 782f36cd52..4c0882b04a 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -117,10 +117,9 @@ private: { } - template < - class... Args, - class = std::enable_if_t>> + template Element(time_point const& when, Args&&... args) + requires(std::is_constructible_v) : value(std::forward(args)...), when(when) { } @@ -841,35 +840,25 @@ public: // //-------------------------------------------------------------------------- - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - at(K const& k); + at(K const& k) + requires(MaybeMap && !MaybeMulti); - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional::type const& - at(K const& k) const; + at(K const& k) const + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key const& key); + operator[](Key const& key) + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key&& key); + operator[](Key&& key) + requires(MaybeMap && !MaybeMulti); //-------------------------------------------------------------------------- // @@ -967,28 +956,32 @@ public: // map, set template auto - insert(value_type const& value) -> std::enable_if_t>; + insert(value_type const& value) -> std::pair + requires(!MaybeMulti); // multimap, multiset template auto - insert(value_type const& value) -> std::enable_if_t; + insert(value_type const& value) -> iterator + requires MaybeMulti; // map, set template auto - insert(value_type&& value) - -> std::enable_if_t>; + insert(value_type&& value) -> std::pair + requires(!MaybeMulti && !MaybeMap); // multimap, multiset template auto - insert(value_type&& value) -> std::enable_if_t; + insert(value_type&& value) -> iterator + requires(MaybeMulti && !MaybeMap); // map, set template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type const& value) + requires(!MaybeMulti) { // Hint is ignored but we provide the interface so // callers may use ordered and unordered interchangeably. @@ -997,8 +990,9 @@ public: // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type const& value) + requires MaybeMulti { // VFALCO TODO The hint could be used to let // the client order equal ranges @@ -1007,8 +1001,9 @@ public: // map, set template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type&& value) + requires(!MaybeMulti) { // Hint is ignored but we provide the interface so // callers may use ordered and unordered interchangeably. @@ -1017,8 +1012,9 @@ public: // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type&& value) + requires MaybeMulti { // VFALCO TODO The hint could be used to let // the client order equal ranges @@ -1027,20 +1023,18 @@ public: // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplace(std::forward

(value)); } // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(const_iterator hint, P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplaceHint(hint, std::forward

(value)); } @@ -1061,23 +1055,26 @@ public: // set, map template auto - emplace(Args&&... args) -> std::enable_if_t>; + emplace(Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template auto - emplace(Args&&... args) -> std::enable_if_t; + emplace(Args&&... args) -> iterator + requires MaybeMulti; // set, map template auto - emplaceHint(const_iterator /*hint*/, Args&&... args) - -> std::enable_if_t>; + emplaceHint(const_iterator /*hint*/, Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template - std::enable_if_t + iterator emplaceHint(const_iterator /*hint*/, Args&&... args) + requires MaybeMulti { // VFALCO TODO The hint could be used for multi, to let // the client order equal ranges @@ -1308,7 +1305,7 @@ public: class OtherHash, class OtherAllocator, bool MaybeMulti = IsMulti> - std::enable_if_t + bool operator==(AgedUnorderedContainer< false, OtherIsMap, @@ -1317,7 +1314,8 @@ public: OtherDuration, OtherHash, KeyEqual, - OtherAllocator> const& other) const; + OtherAllocator> const& other) const + requires(!MaybeMulti); template < bool OtherIsMap, @@ -1327,7 +1325,7 @@ public: class OtherHash, class OtherAllocator, bool MaybeMulti = IsMulti> - std::enable_if_t + bool operator==(AgedUnorderedContainer< true, OtherIsMap, @@ -1336,7 +1334,8 @@ public: OtherDuration, OtherHash, KeyEqual, - OtherAllocator> const& other) const; + OtherAllocator> const& other) const + requires MaybeMulti; template < bool OtherIsMulti, @@ -1381,13 +1380,14 @@ private: // map, set template auto - insertUnchecked(value_type const& value) - -> std::enable_if_t>; + insertUnchecked(value_type const& value) -> std::pair + requires(!MaybeMulti); // multimap, multiset template auto - insertUnchecked(value_type const& value) -> std::enable_if_t; + insertUnchecked(value_type const& value) -> iterator + requires MaybeMulti; template void @@ -1428,8 +1428,9 @@ private: template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t + void swapData(AgedUnorderedContainer& other) noexcept + requires MaybePropagate { std::swap(config_.hashFunction(), other.config_.hashFunction()); std::swap(config_.keyEq(), other.config_.keyEq()); @@ -1439,8 +1440,9 @@ private: template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t + void swapData(AgedUnorderedContainer& other) noexcept + requires(!MaybePropagate) { std::swap(config_.hashFunction(), other.config_.hashFunction()); std::swap(config_.keyEq(), other.config_.keyEq()); @@ -2094,9 +2096,10 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional_t& AgedUnorderedContainer::at(K const& k) + requires(MaybeMap && !MaybeMulti) { auto const iter( cont_.find(k, std::cref(config_.hashFunction()), std::cref(config_.keyValueEqual()))); @@ -2114,10 +2117,11 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional::type const& AgedUnorderedContainer::at( K const& k) const + requires(MaybeMap && !MaybeMulti) { auto const iter( cont_.find(k, std::cref(config_.hashFunction()), std::cref(config_.keyValueEqual()))); @@ -2135,10 +2139,11 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional_t& AgedUnorderedContainer::operator[]( Key const& key) + requires(MaybeMap && !MaybeMulti) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2164,10 +2169,11 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional_t& AgedUnorderedContainer::operator[]( Key&& key) + requires(MaybeMap && !MaybeMulti) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2220,7 +2226,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type const& value) -> std::enable_if_t> + value_type const& value) -> std::pair + requires(!MaybeMulti) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2249,7 +2256,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires MaybeMulti { maybeRehash(1); Element* const p(newElement(value)); @@ -2271,7 +2279,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type&& value) -> std::enable_if_t> + value_type&& value) -> std::pair + requires(!MaybeMulti && !MaybeMap) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2300,7 +2309,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type&& value) -> std::enable_if_t + value_type&& value) -> iterator + requires(MaybeMulti && !MaybeMap) { maybeRehash(1); Element* const p(newElement(std::move(value))); @@ -2323,7 +2333,8 @@ template < template auto AgedUnorderedContainer::emplace( - Args&&... args) -> std::enable_if_t> + Args&&... args) -> std::pair + requires(!MaybeMulti) { maybeRehash(1); // VFALCO NOTE Its unfortunate that we need to @@ -2352,7 +2363,8 @@ template < template auto AgedUnorderedContainer::emplace( - Args&&... args) -> typename std::enable_if>::type + Args&&... args) -> std::pair + requires(!maybe_multi) { maybe_rehash(1); // VFALCO NOTE Its unfortunate that we need to @@ -2388,7 +2400,8 @@ template < template auto AgedUnorderedContainer::emplace( - Args&&... args) -> std::enable_if_t + Args&&... args) -> iterator + requires MaybeMulti { maybeRehash(1); Element* const p(newElement(std::forward(args)...)); @@ -2411,7 +2424,8 @@ template auto AgedUnorderedContainer::emplaceHint( const_iterator /*hint*/, - Args&&... args) -> std::enable_if_t> + Args&&... args) -> std::pair + requires(!MaybeMulti) { maybeRehash(1); // VFALCO NOTE Its unfortunate that we need to @@ -2562,7 +2576,7 @@ template < class OtherHash, class OtherAllocator, bool MaybeMulti> -std::enable_if_t +bool AgedUnorderedContainer::operator==( AgedUnorderedContainer< false, @@ -2573,6 +2587,7 @@ AgedUnorderedContainer OtherHash, KeyEqual, OtherAllocator> const& other) const + requires(!MaybeMulti) { if (size() != other.size()) return false; @@ -2602,7 +2617,7 @@ template < class OtherHash, class OtherAllocator, bool MaybeMulti> -std::enable_if_t +bool AgedUnorderedContainer::operator==( AgedUnorderedContainer< true, @@ -2613,6 +2628,7 @@ AgedUnorderedContainer OtherHash, KeyEqual, OtherAllocator> const& other) const + requires MaybeMulti { if (size() != other.size()) return false; @@ -2649,7 +2665,8 @@ template < template auto AgedUnorderedContainer::insertUnchecked( - value_type const& value) -> std::enable_if_t> + value_type const& value) -> std::pair + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check( @@ -2677,7 +2694,8 @@ template < template auto AgedUnorderedContainer::insertUnchecked( - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires MaybeMulti { Element* const p(newElement(value)); chronological.list_.push_back(*p); diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 8faf90f53d..1162d83078 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -29,16 +29,18 @@ struct LexicalCast explicit LexicalCast() = default; template - std::enable_if_t, bool> + bool operator()(std::string& out, Arithmetic in) + requires(std::is_arithmetic_v) { out = std::to_string(in); return true; } template - std::enable_if_t, bool> + bool operator()(std::string& out, Enumeration in) + requires(std::is_enum_v) { out = std::to_string(static_cast>(in)); return true; @@ -56,8 +58,9 @@ struct LexicalCast "beast::LexicalCast can only be used with integral types"); template - std::enable_if_t && !std::is_same_v, bool> + bool operator()(Integral& out, std::string_view in) const + requires(std::is_integral_v && !std::is_same_v) { auto first = in.data(); auto last = in.data() + in.size(); diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h index 5f77f41e5d..da499dc612 100644 --- a/include/xrpl/beast/hash/hash_append.h +++ b/include/xrpl/beast/hash/hash_append.h @@ -200,26 +200,29 @@ struct IsContiguouslyHashable // scalars template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, T const& t) noexcept + requires(IsContiguouslyHashable::value) { // NOLINTNEXTLINE(bugprone-sizeof-expression) h(static_cast(std::addressof(t)), sizeof(t)); } template -inline std::enable_if_t< - !IsContiguouslyHashable::value && - (std::is_integral_v || std::is_pointer_v || std::is_enum_v)> +inline void hash_append(Hasher& h, T t) noexcept + requires( + !IsContiguouslyHashable::value && + (std::is_integral_v || std::is_pointer_v || std::is_enum_v)) { detail::reverseBytes(t); h(std::addressof(t), sizeof(t)); } template -inline std::enable_if_t> +inline void hash_append(Hasher& h, T t) noexcept + requires(std::is_floating_point_v) { if (t == 0) t = 0; @@ -239,36 +242,44 @@ hash_append(Hasher& h, std::nullptr_t) noexcept // Forward declarations for ADL purposes template -std::enable_if_t::value> -hash_append(Hasher& h, T (&a)[N]) noexcept; +void +hash_append(Hasher& h, T (&a)[N]) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::basic_string const& s) noexcept; +void +hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::basic_string const& s) noexcept; +void +hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(IsContiguouslyHashable::value); template -std::enable_if_t, Hasher>::value> -hash_append(Hasher& h, std::pair const& p) noexcept; +void +hash_append(Hasher& h, std::pair const& p) noexcept + requires(!IsContiguouslyHashable, Hasher>::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::vector const& v) noexcept; +void +hash_append(Hasher& h, std::vector const& v) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::vector const& v) noexcept; +void +hash_append(Hasher& h, std::vector const& v) noexcept + requires(IsContiguouslyHashable::value); template -std::enable_if_t, Hasher>::value> -hash_append(Hasher& h, std::array const& a) noexcept; +void +hash_append(Hasher& h, std::array const& a) noexcept + requires(!IsContiguouslyHashable, Hasher>::value); template -std::enable_if_t, Hasher>::value> -hash_append(Hasher& h, std::tuple const& t) noexcept; +void +hash_append(Hasher& h, std::tuple const& t) noexcept + requires(!IsContiguouslyHashable, Hasher>::value); template void @@ -279,11 +290,13 @@ void hash_append(Hasher& h, std::unordered_set const& s); template -std::enable_if_t::value> -hash_append(Hasher& h, boost::container::flat_set const& v) noexcept; +void +hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, boost::container::flat_set const& v) noexcept; +void +hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(IsContiguouslyHashable::value); template void hash_append(Hasher& h, T0 const& t0, T1 const& t1, T const&... t) noexcept; @@ -291,8 +304,9 @@ hash_append(Hasher& h, T0 const& t0, T1 const& t1, T const&... t) noexcept; // c-array template -std::enable_if_t::value> +void hash_append(Hasher& h, T (&a)[N]) noexcept + requires(!IsContiguouslyHashable::value) { for (auto const& t : a) hash_append(h, t); @@ -301,8 +315,9 @@ hash_append(Hasher& h, T (&a)[N]) noexcept // basic_string template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(!IsContiguouslyHashable::value) { for (auto c : s) hash_append(h, c); @@ -310,8 +325,9 @@ hash_append(Hasher& h, std::basic_string const& s) noexcep } template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(IsContiguouslyHashable::value) { h(s.data(), s.size() * sizeof(CharT)); hash_append(h, s.size()); @@ -320,8 +336,9 @@ hash_append(Hasher& h, std::basic_string const& s) noexcep // pair template -inline std::enable_if_t, Hasher>::value> +inline void hash_append(Hasher& h, std::pair const& p) noexcept + requires(!IsContiguouslyHashable, Hasher>::value) { hash_append(h, p.first, p.second); } @@ -329,8 +346,9 @@ hash_append(Hasher& h, std::pair const& p) noexcept // vector template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::vector const& v) noexcept + requires(!IsContiguouslyHashable::value) { for (auto const& t : v) hash_append(h, t); @@ -338,8 +356,9 @@ hash_append(Hasher& h, std::vector const& v) noexcept } template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::vector const& v) noexcept + requires(IsContiguouslyHashable::value) { h(v.data(), v.size() * sizeof(T)); hash_append(h, v.size()); @@ -348,23 +367,26 @@ hash_append(Hasher& h, std::vector const& v) noexcept // array template -std::enable_if_t, Hasher>::value> +void hash_append(Hasher& h, std::array const& a) noexcept + requires(!IsContiguouslyHashable, Hasher>::value) { for (auto const& t : a) hash_append(h, t); } template -std::enable_if_t::value> +void hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(!IsContiguouslyHashable::value) { for (auto const& t : v) hash_append(h, t); } template -std::enable_if_t::value> +void hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(IsContiguouslyHashable::value) { h(&(v.begin()), v.size() * sizeof(Key)); } @@ -395,8 +417,9 @@ tuple_hash(Hasher& h, std::tuple const& t, std::index_sequence) noex } // namespace detail template -inline std::enable_if_t, Hasher>::value> +inline void hash_append(Hasher& h, std::tuple const& t) noexcept + requires(!IsContiguouslyHashable, Hasher>::value) { detail::tuple_hash(h, t, std::index_sequence_for{}); } diff --git a/include/xrpl/beast/hash/xxhasher.h b/include/xrpl/beast/hash/xxhasher.h index 978bbc6917..73dbb8e8ab 100644 --- a/include/xrpl/beast/hash/xxhasher.h +++ b/include/xrpl/beast/hash/xxhasher.h @@ -124,14 +124,18 @@ public: } } - template >* = nullptr> - explicit Xxhasher(Seed seed) : seed_(seed) + template + explicit Xxhasher(Seed seed) + requires(std::is_unsigned_v) + : seed_(seed) { resetBuffers(); } - template >* = nullptr> - Xxhasher(Seed seed, Seed) : seed_(seed) + template + Xxhasher(Seed seed, Seed) + requires(std::is_unsigned_v) + : seed_(seed) { resetBuffers(); } diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h index ee7a5e2434..5bd9d8bc5c 100644 --- a/include/xrpl/beast/utility/rngfill.h +++ b/include/xrpl/beast/utility/rngfill.h @@ -3,7 +3,6 @@ #include #include #include -#include namespace beast { @@ -33,12 +32,10 @@ rngfill(void* const buffer, std::size_t const bytes, Generator& g) } } -template < - class Generator, - std::size_t N, - class = std::enable_if_t> +template void rngfill(std::array& a, Generator& g) + requires(N % sizeof(typename Generator::result_type) == 0) { using result_type = Generator::result_type; auto i = N / sizeof(result_type); diff --git a/include/xrpl/core/JobQueue.h b/include/xrpl/core/JobQueue.h index 14170a39be..e4b64546f3 100644 --- a/include/xrpl/core/JobQueue.h +++ b/include/xrpl/core/JobQueue.h @@ -154,16 +154,14 @@ public: @param type The type of job. @param name Name of the job. - @param jobHandler Lambda with signature void (Job&). Called when the - job is executed. + @param jobHandler Callable with signature void(). Called when the job is executed. @return true if jobHandler added to queue. */ - template < - typename JobHandler, - typename = std::enable_if_t()()), void>>> + template bool addJob(JobType type, std::string const& name, JobHandler&& jobHandler) + requires(std::is_void_v>) { if (auto optionalCountedJob = jobCounter_.wrap(std::forward(jobHandler))) { diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h b/include/xrpl/ledger/helpers/DirectoryHelpers.h index 76a5f3bdad..a95b9bc95a 100644 --- a/include/xrpl/ledger/helpers/DirectoryHelpers.h +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h @@ -19,11 +19,7 @@ namespace xrpl { namespace detail { -template < - class V, - class N, - class = std::enable_if_t< - std::is_same_v, SLE> && std::is_base_of_v>> +template bool internalDirNext( V& view, @@ -31,6 +27,7 @@ internalDirNext( std::shared_ptr& page, unsigned int& index, uint256& entry) + requires(std::is_same_v, SLE> && std::is_base_of_v) { auto const& svIndexes = page->getFieldV256(sfIndexes); XRPL_ASSERT(index <= svIndexes.size(), "xrpl::detail::internalDirNext : index inside range"); @@ -68,11 +65,7 @@ internalDirNext( return true; } -template < - class V, - class N, - class = std::enable_if_t< - std::is_same_v, SLE> && std::is_base_of_v>> +template bool internalDirFirst( V& view, @@ -80,6 +73,7 @@ internalDirFirst( std::shared_ptr& page, unsigned int& index, uint256& entry) + requires(std::is_same_v, SLE> && std::is_base_of_v) { if constexpr (std::is_const_v) { diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h index 2b26d7e404..51b50a084c 100644 --- a/include/xrpl/net/HTTPClientSSLContext.h +++ b/include/xrpl/net/HTTPClientSSLContext.h @@ -83,13 +83,12 @@ public: * * @return error_code indicating failures, if any */ - template < - class T, - class = std::enable_if_t< - std::is_same_v> || - std::is_same_v>>> + template boost::system::error_code preConnectVerify(T& strm, std::string const& host) + requires( + std::is_same_v> || + std::is_same_v>) { boost::system::error_code ec; if (!SSL_set_tlsext_host_name(strm.native_handle(), host.c_str())) @@ -103,11 +102,7 @@ public: return ec; } - template < - class T, - class = std::enable_if_t< - std::is_same_v> || - std::is_same_v>>> + template /** * @brief invoked after connect/async_connect but before sending data * on an ssl stream - to setup name verification. @@ -117,6 +112,9 @@ public: */ boost::system::error_code postConnectVerify(T& strm, std::string const& host) + requires( + std::is_same_v> || + std::is_same_v>) { boost::system::error_code ec; diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h index 6102c0cf2d..5a65545d3a 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/varint.h @@ -68,9 +68,10 @@ readVarint(void const* buf, std::size_t buflen, std::size_t& t) return used; } -template >* = nullptr> +template std::size_t sizeVarint(T v) + requires(std::is_unsigned_v) { std::size_t n = 0; do @@ -100,9 +101,10 @@ writeVarint(void* p0, std::size_t v) // input stream -template >* = nullptr> +template void read(nudb::detail::istream& is, std::size_t& u) + requires(std::is_same_v) { auto p0 = is(1); auto p1 = p0; @@ -113,9 +115,10 @@ read(nudb::detail::istream& is, std::size_t& u) // output stream -template >* = nullptr> +template void write(nudb::detail::ostream& os, std::size_t t) + requires(std::is_same_v) { writeVarint(os.data(sizeVarint(t)), t); } diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index ed39e65026..573bb6dad8 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -32,17 +32,13 @@ public: STArray() = default; STArray(STArray const&) = default; - template < - class Iter, - class = std::enable_if_t< - std::is_convertible_v::reference, STObject>>> - explicit STArray(Iter first, Iter last); + template + explicit STArray(Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>); - template < - class Iter, - class = std::enable_if_t< - std::is_convertible_v::reference, STObject>>> - STArray(SField const& f, Iter first, Iter last); + template + STArray(SField const& f, Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>); STArray& operator=(STArray const&) = default; @@ -170,13 +166,17 @@ private: friend class detail::STVar; }; -template -STArray::STArray(Iter first, Iter last) : v_(first, last) +template +STArray::STArray(Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>) + : v_(first, last) { } -template -STArray::STArray(SField const& f, Iter first, Iter last) : STBase(f), v_(first, last) +template +STArray::STArray(SField const& f, Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>) + : STBase(f), v_(first, last) { } diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index a60e8f7fe8..c96086b1d0 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -556,8 +556,9 @@ public: operator=(ValueProxy const&) = delete; template - std::enable_if_t, ValueProxy&> - operator=(U&& u); + ValueProxy& + operator=(U&& u) + requires(std::is_assignable_v); // Convenience operators for value types supporting // arithmetic operations @@ -691,8 +692,9 @@ public: operator=(optional_type const& v); template - std::enable_if_t, OptionalProxy&> - operator=(U&& u); + OptionalProxy& + operator=(U&& u) + requires(std::is_assignable_v); private: friend class STObject; @@ -798,8 +800,9 @@ STObject::Proxy::assign(U&& u) template template -std::enable_if_t, STObject::ValueProxy&> +STObject::ValueProxy& STObject::ValueProxy::operator=(U&& u) + requires(std::is_assignable_v) { this->assign(std::forward(u)); return *this; @@ -902,8 +905,9 @@ STObject::OptionalProxy::operator=(optional_type const& v) -> OptionalProxy& template template -std::enable_if_t, STObject::OptionalProxy&> +STObject::OptionalProxy& STObject::OptionalProxy::operator=(U&& u) + requires(std::is_assignable_v) { this->assign(std::forward(u)); return *this; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index b29cb11e15..54b081f358 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -411,7 +411,7 @@ TERtoInt(TECcodes v) //------------------------------------------------------------------------------ // Template class that is specific to selected ranges of error codes. The -// Trait tells std::enable_if which ranges are allowed. +// Trait tells the requires-clause which ranges are allowed. template