From 4387aac1a5275ad987412b63b61e7aed2ef93e1a Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 15 Jun 2026 15:55:43 +0100 Subject: [PATCH 001/121] chore: Remove conan patch in nix (#7534) --- flake.lock | 13 +++++++------ flake.nix | 2 +- nix/devshell.nix | 22 +--------------------- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/flake.lock b/flake.lock index f8553af703..80243ccf15 100644 --- a/flake.lock +++ b/flake.lock @@ -2,17 +2,18 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1780749050, - "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", + "lastModified": 1781173989, + "narHash": "sha256-fnzKKPvS+oieI/pTzotA5tkoM47EB1NpaBcgk4R97hE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", + "rev": "8c91a71d13451abc40eb9dae8910f972f979852f", "type": "github" }, "original": { - "id": "nixpkgs", - "ref": "nixos-unstable", - "type": "indirect" + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs-custom-glibc": { diff --git a/flake.nix b/flake.nix index 3b3ec7ea08..c52f4d050e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "Nix related things for xrpld"; inputs = { - nixpkgs.url = "nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; # nixpkgs snapshot (2020-06-30) that shipped glibc 2.31 as the primary # version — matches the system libc on Ubuntu 20.04 LTS. Imported # manually (flake = false) because this revision predates nixpkgs' diff --git a/nix/devshell.nix b/nix/devshell.nix index 105033eb06..1bd7ea4c0c 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -1,26 +1,6 @@ { pkgs, ... }: let - # conan is in the binary cache for Linux but not for Darwin, so on Darwin - # it is always built from source — and its bundled test suite is unreliable - # in the sandbox: `test_qbsprofile_rcflags` needs gcc (absent on Darwin, see - # https://github.com/NixOS/nixpkgs/pull/528995) and the patch tests are - # flaky from source. We only use conan as a build tool, so skip its tests on - # Darwin. Scoped to the dev shell (not the CI env, which builds conan on - # Linux from the cache). Drop once the fix reaches nixos-unstable and the - # lock is bumped. - pkgs_patched = - if pkgs.stdenv.isDarwin then - pkgs.extend ( - final: prev: { - conan = prev.conan.overridePythonAttrs (_: { - doCheck = false; - }); - } - ) - else - pkgs; - - inherit (import ./packages.nix { pkgs = pkgs_patched; }) commonPackages; + inherit (import ./packages.nix { inherit pkgs; }) commonPackages; # Supported compiler versions gccVersion = pkgs.lib.range 13 15; From f5985e73ecce0de85eae73e54405d62fa16a091b Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 15 Jun 2026 10:55:56 -0400 Subject: [PATCH 002/121] fix: Always charge peer on strand (#7422) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- src/xrpld/overlay/detail/PeerImp.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 323dc14673..cda576add0 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -58,7 +58,6 @@ #include #include #include -#include #include #include #include @@ -68,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -392,13 +392,15 @@ PeerImp::removeTxQueue(uint256 const& hash) void PeerImp::charge(Resource::Charge const& fee, std::string const& context) { - if ((usage_.charge(fee, context) == Resource::Disposition::Drop) && - usage_.disconnect(pJournal_) && strand_.running_in_this_thread()) - { - // Sever the connection - overlay_.incPeerDisconnectCharges(); - fail("charge: Resources"); - } + dispatch(strand_, [this, self = shared_from_this(), fee, context]() { + if (usage_.charge(fee, context) == Resource::Disposition::Drop && + usage_.disconnect(pJournal_)) + { + // Sever the connection. + overlay_.incPeerDisconnectCharges(); + fail("charge: Resources"); + } + }); } //------------------------------------------------------------------------------ From b34aa84e5aa0ba8741e38f037350677239f9da12 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:31:22 -0400 Subject: [PATCH 003/121] fix: Check Fee-Free Division by Zero in AMMWithdraw singleWithdrawEPrice (#6989) --- .../tx/transactors/dex/AMMWithdraw.cpp | 11 +++++--- src/test/app/AMM_test.cpp | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index e57f8558ff..d3a6c9c74c 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -1091,10 +1091,13 @@ AMMWithdraw::singleWithdrawEPrice( // t = T*(T + A*E*(f - 2))/(T*f - A*E) Number const ae = amountBalance * ePrice; auto const f = getFee(tfee); - auto tokNoRoundCb = [&] { - return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); - }; - auto tokProdCb = [&] { return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); }; + auto const denom = lptAMMBalance * f - ae; + // fixCleanup3_3_0: guard against division by zero + // when ePrice == lptAMMBalance*f/amountBalance + if (view.rules().enabled(fixCleanup3_3_0) && denom == beast::kZero) + return {tecAMM_FAILED, STAmount{}}; + auto tokNoRoundCb = [&] { return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / denom; }; + auto tokProdCb = [&] { return (lptAMMBalance + ae * (f - 2)) / denom; }; auto const tokensAdj = getRoundedLPTokens(view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No); if (tokensAdj <= beast::kZero) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index e3a1cc935f..1b54c2aab9 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -2229,6 +2229,31 @@ private: ammAlice.withdraw(alice_, XRPAmount{9'999'999'999}); BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100})); }); + + // singleWithdrawEPrice: crafted ePrice = lptAMMBalance*f/amountBalance + // makes the denominator (T*f - A*E) exactly zero. + // Pre-fixCleanup3_3_0: std::overflow_error escapes to the + // transactor backstop and is returned as tefEXCEPTION. + // Post-fixCleanup3_3_0: denominator check returns tecAMM_FAILED. + // + // Pool: USD(100)/EUR(100), baseFee=1000 (1%). + // Alice is the creator so her discounted fee is 100 (0.1%), f=0.001. + // ePrice = lptAMMBalance(100) * f(0.001) / amountBalance(100) = 0.001 + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const err = + env.enabled(fixCleanup3_3_0) ? Ter(tecAMM_FAILED) : Ter(tefEXCEPTION); + ammAlice.withdraw( + WithdrawArg{ + .account = alice_, + .asset1Out = USD(0), + .maxEP = IOUAmount{1, -3}, // ePrice=0.001 → denom=0 + .err = err}); + }, + {{USD(100), EUR(100)}}, + 1000, + std::nullopt, + {all - fixCleanup3_3_0, all}); } void From fe4c8ae82a76ccd853171ce7547a4e1785d0de87 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 15 Jun 2026 20:04:33 +0100 Subject: [PATCH 004/121] build: Add ClangBuildAnalyzer to Nix (#7538) Co-authored-by: Bart --- nix/docker/check-tools.sh | 1 + nix/packages.nix | 1 + 2 files changed, 2 insertions(+) diff --git a/nix/docker/check-tools.sh b/nix/docker/check-tools.sh index 276e5977ff..a46c2dd997 100755 --- a/nix/docker/check-tools.sh +++ b/nix/docker/check-tools.sh @@ -6,6 +6,7 @@ ccache --version clang --version clang++ --version clang-format --version +ClangBuildAnalyzer --version cmake --version conan --version curl --version diff --git a/nix/packages.nix b/nix/packages.nix index fc4eff679e..5a7f20ec49 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -9,6 +9,7 @@ in { commonPackages = with pkgs; [ ccache + clangbuildanalyzer cmake conan curlMinimal # needed for codecov/codecov-action From 2df96b155061bbd23a6477776ea3af3f0cd26e94 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:25:37 +0100 Subject: [PATCH 005/121] fix: Silence UBSan diagnostics in the ubsan build config (#7531) Co-authored-by: Claude Opus 4.8 --- .../workflows/reusable-build-test-config.yml | 35 +++--- .../suppressions/runtime-ubsan-options.txt | 2 +- sanitizers/suppressions/ubsan.supp | 106 +++++++++++++----- src/xrpld/rpc/handlers/ledger/Ledger.cpp | 15 ++- 4 files changed, 112 insertions(+), 46 deletions(-) diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 8cb5f8c46a..28d317e4dd 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -164,6 +164,27 @@ jobs: ${CMAKE_ARGS} \ .. + # Export the sanitizer options before any instrumented binary runs. The + # protocol code-gen and build steps below invoke instrumented dependency + # tools (protoc, grpc), so setting UBSAN_OPTIONS here lets the UBSan + # suppression list silence their diagnostics too, not just at test time. + # GITHUB_WORKSPACE (not the github.workspace context) is used so the path + # resolves correctly inside the container job. + - name: Set sanitizer options + if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }} + env: + CONFIG_NAME: ${{ inputs.config_name }} + run: | + SUPP="${GITHUB_WORKSPACE}/sanitizers/suppressions" + ASAN_OPTS="include=${SUPP}/runtime-asan-options.txt:suppressions=${SUPP}/asan.supp" + if [[ "${CONFIG_NAME}" == *gcc* ]]; then + ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0" + fi + echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV} + echo "TSAN_OPTIONS=include=${SUPP}/runtime-tsan-options.txt:suppressions=${SUPP}/tsan.supp" >>${GITHUB_ENV} + echo "UBSAN_OPTIONS=include=${SUPP}/runtime-ubsan-options.txt:suppressions=${SUPP}/ubsan.supp" >>${GITHUB_ENV} + echo "LSAN_OPTIONS=include=${SUPP}/runtime-lsan-options.txt:suppressions=${SUPP}/lsan.supp" >>${GITHUB_ENV} + - name: Check protocol autogen files are up-to-date working-directory: ${{ env.BUILD_DIR }} env: @@ -279,20 +300,6 @@ jobs: run: | ./xrpld --version | grep libvoidstar - - name: Set sanitizer options - if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }} - env: - CONFIG_NAME: ${{ inputs.config_name }} - run: | - ASAN_OPTS="include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-asan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp" - if [[ "${CONFIG_NAME}" == *gcc* ]]; then - ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0" - fi - echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV} - echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >>${GITHUB_ENV} - echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >>${GITHUB_ENV} - echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >>${GITHUB_ENV} - - name: Run the separate tests if: ${{ !inputs.build_only }} working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} diff --git a/sanitizers/suppressions/runtime-ubsan-options.txt b/sanitizers/suppressions/runtime-ubsan-options.txt index fcfccf7bae..4b48efbe08 100644 --- a/sanitizers/suppressions/runtime-ubsan-options.txt +++ b/sanitizers/suppressions/runtime-ubsan-options.txt @@ -1 +1 @@ -halt_on_error=false +halt_on_error=true diff --git a/sanitizers/suppressions/ubsan.supp b/sanitizers/suppressions/ubsan.supp index 88d8e82e33..7e3e02f855 100644 --- a/sanitizers/suppressions/ubsan.supp +++ b/sanitizers/suppressions/ubsan.supp @@ -72,7 +72,7 @@ vptr:boost # Google protobuf - intentional overflows in hash functions undefined:protobuf -unsigned-integer-overflow:google/protobuf/stubs/stringpiece.h +unsigned-integer-overflow:protobuf # gRPC intentional overflows in timer calculations unsigned-integer-overflow:grpc @@ -102,47 +102,103 @@ undefined:nudb # Snappy compression library intentional overflows unsigned-integer-overflow:snappy.cc -# Abseil intentional overflows -unsigned-integer-overflow:absl/strings/numbers.cc -unsigned-integer-overflow:absl/strings/internal/cord_rep_flat.h -unsigned-integer-overflow:absl/base/internal/low_level_alloc.cc -unsigned-integer-overflow:absl/hash/internal/hash.h -unsigned-integer-overflow:absl/container/internal/raw_hash_set.h +# Abseil intentional overflows in hashing, RNG and time arithmetic. +# Matched at library scope (like boost above): the wraparound is by design +# across many absl files (hash mixing, raw_hash_set probing, duration math, +# int128, uniform_int_distribution), so listing individual files just churns. +unsigned-integer-overflow:absl # Standard library intentional overflows unsigned-integer-overflow:basic_string.h +unsigned-integer-overflow:bits/align.h +unsigned-integer-overflow:bits/basic_string.tcc unsigned-integer-overflow:bits/chrono.h unsigned-integer-overflow:bits/random.h unsigned-integer-overflow:bits/random.tcc unsigned-integer-overflow:bits/stl_algobase.h +unsigned-integer-overflow:bits/string_view.tcc unsigned-integer-overflow:bits/uniform_int_dist.h unsigned-integer-overflow:string_view unsigned-integer-overflow:__random/seed_seq.h unsigned-integer-overflow:__charconv/traits.h unsigned-integer-overflow:__chrono/duration.h +# libstdc++ (std::__bit_ceil etc.) negates an unsigned width; is a +# distinct header from the bits/ directory so it needs its own entry. +unsigned-integer-overflow:include/c++/*/bit # ============================================================================= # Rippled code suppressions # ============================================================================= -# Signed integer negation (-value) in amount types. -# INT64_MIN cannot occur in practice due to domain invariants (mantissa ranges -# are well within int64_t bounds), but UBSan flags the pattern as potential -# signed overflow. Narrowed to operator- to avoid suppressing unrelated -# overflows anywhere in a stack trace containing these type names. -signed-integer-overflow:operator-*IOUAmount* -signed-integer-overflow:operator-*XRPAmount* -signed-integer-overflow:operator-*MPTAmount* -signed-integer-overflow:operator-*STAmount* +# These suppressions are keyed by SOURCE FILE, not function name. This UBSan +# build runs without symbol information, so the runtime only knows the +# file:line of each report, never the enclosing function — function-name +# patterns silently never match. Each entry below is therefore scoped to the +# file whose arithmetic is intentional; the comment names the specific +# construct. -# STAmount::operator+ signed addition — operands are bounded by total supply -# (~10^17 for XRP, ~10^18 for MPT) so overflow cannot occur in practice. -signed-integer-overflow:operator+*STAmount* +# STAmount amount-type arithmetic. Unary negation of the mantissa in xrp()/ +# iou()/mpt()/canonicalize() and getInt64Value, plus bounded +/- on amounts: +# INT64_MIN cannot occur because canonicalize() keeps the mantissa well within +# int64_t, and operands are bounded by total supply (~10^17 XRP, ~10^18 MPT). +signed-integer-overflow:protocol/STAmount.cpp -# STAmount::getRate uses unsigned shift and addition -unsigned-integer-overflow:*STAmount*getRate* -# STAmount::serialize uses unsigned bitwise operations -unsigned-integer-overflow:*STAmount*serialize* +# nft::cipheredTaxon uses intentional uint32 wraparound (LCG permutation); +# the helper lives in the generated protocol header nft.h. +unsigned-integer-overflow:protocol/nft.h -# nft::cipheredTaxon uses intentional uint32 wraparound (LCG permutation) -unsigned-integer-overflow:cipheredTaxon +# STPathElement::getHash multiplies/adds accumulators (non-secure, speed-first). +unsigned-integer-overflow:protocol/STPathSet.cpp + +# beast XorShiftEngine PRNG and murmurhash3 mixing wrap by design. +unsigned-integer-overflow:beast/xor_shift_engine.h + +# Number::normalizeToRange multiplies the mantissa by powers of ten; the result +# is intentionally allowed to wrap while searching for the in-range value. +unsigned-integer-overflow:basics/Number.h + +# Counter / sequence arithmetic with intentional unsigned wraparound, each +# guarded by an explicit overflow or domain check at the call site: +# base_uint operator++/-- wrap by definition; +# ApplyView::insertPage ++page is asserted to wrap to 0 (page exhaustion); +# confineOwnerCount documents "overflow is well defined on unsigned"; +# NFTokenMint checks tokenSeq + 1u == 0u; AmendmentTable does (seq - 1) / 256. +unsigned-integer-overflow:basics/base_uint.h +unsigned-integer-overflow:ledger/ApplyView.cpp +unsigned-integer-overflow:ledger/helpers/AccountRootHelpers.cpp +unsigned-integer-overflow:tx/transactors/nft/NFTokenMint.cpp +unsigned-integer-overflow:app/misc/detail/AmendmentTable.cpp + +# Sentinel / bounded subtractions that wrap by design (loop counters, reverse +# iteration, "not found" sentinels, balance math bounded by issuance invariants, +# base58/base64 codec index math, hash-router and role bit math). +unsigned-integer-overflow:shamap/SHAMap.cpp +unsigned-integer-overflow:protocol/Permissions.cpp +unsigned-integer-overflow:protocol/tokens.cpp +unsigned-integer-overflow:basics/base64.cpp +unsigned-integer-overflow:json/json_value.cpp +unsigned-integer-overflow:app/misc/NetworkOPs.cpp +unsigned-integer-overflow:rpc/detail/Role.cpp +unsigned-integer-overflow:tx/transactors/oracle/OracleSet.cpp +unsigned-integer-overflow:ledger/helpers/MPTokenHelpers.cpp +unsigned-integer-overflow:crypto/RFC1751.cpp +unsigned-integer-overflow:tx/paths/detail/StrandFlow.h +unsigned-integer-overflow:protocol/STObject.h + +# GetAggregatePrice negates an unsigned trim count to step a reverse iterator; +# trimCount is bounded by the price set size. +unsigned-integer-overflow:rpc/handlers/orderbook/GetAggregatePrice.cpp + +# Test-only intentional overflow/underflow in fixture and unit-test arithmetic. +unsigned-integer-overflow:tests/libxrpl/basics/RangeSet.cpp +unsigned-integer-overflow:test/app/Batch_test.cpp +unsigned-integer-overflow:test/app/Invariants_test.cpp +unsigned-integer-overflow:test/app/Loan_test.cpp +unsigned-integer-overflow:test/app/NFToken_test.cpp +unsigned-integer-overflow:test/app/OfferMPT_test.cpp +unsigned-integer-overflow:test/app/Offer_test.cpp +unsigned-integer-overflow:test/app/Path_test.cpp +unsigned-integer-overflow:test/jtx/impl/acctdelete.cpp +unsigned-integer-overflow:test/ledger/SkipList_test.cpp +unsigned-integer-overflow:test/rpc/Subscribe_test.cpp +signed-integer-overflow:test/basics/XRPAmount_test.cpp diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.cpp b/src/xrpld/rpc/handlers/ledger/Ledger.cpp index 5938c8c9c5..23a97a5026 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.cpp +++ b/src/xrpld/rpc/handlers/ledger/Ledger.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include namespace xrpl { @@ -349,13 +350,15 @@ doLedgerGrpc(RPC::GRPCContext& context) auto end = std::chrono::system_clock::now(); auto duration = std::chrono::duration_cast(end - begin).count() * 1.0; + // Guard the per-item rates: an empty ledger has zero objects and/or zero + // transactions, and dividing by zero is undefined for these doubles. + auto const numObjects = response.ledger_objects().objects_size(); + auto const numTxns = response.transactions_list().transactions_size(); + std::string const msPerObj = numObjects > 0 ? std::to_string(duration / numObjects) : "n/a"; + std::string const msPerTxn = numTxns > 0 ? std::to_string(duration / numTxns) : "n/a"; JLOG(context.j.warn()) << __func__ << " - Extract time = " << duration - << " - num objects = " << response.ledger_objects().objects_size() - << " - num txns = " << response.transactions_list().transactions_size() - << " - ms per obj " - << duration / response.ledger_objects().objects_size() - << " - ms per txn " - << duration / response.transactions_list().transactions_size(); + << " - num objects = " << numObjects << " - num txns = " << numTxns + << " - ms per obj " << msPerObj << " - ms per txn " << msPerTxn; return {response, status}; } From 9650fe8a6ecb8344d134e183ac74c15ac58dcc44 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Tue, 26 May 2026 21:04:01 +0100 Subject: [PATCH 006/121] refactor: Use explicit types to help compiler --- .clang-tidy | 1 + src/xrpld/overlay/detail/PeerImp.cpp | 2 +- src/xrpld/overlay/detail/Tuning.h | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index b23d7ccbff..b0da673b13 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -153,6 +153,7 @@ 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 # 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/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 325f8ba038..324e8e6b87 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -2032,7 +2032,7 @@ PeerImp::checkTracking(std::uint32_t validationSeq) void PeerImp::checkTracking(std::uint32_t seq1, std::uint32_t seq2) { - int const diff = std::max(seq1, seq2) - std::min(seq1, seq2); + std::uint32_t const diff = std::max(seq1, seq2) - std::min(seq1, seq2); if (diff < Tuning::kConvergedLedgerLimit) { diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index a0f57ec3d7..97ce3f5188 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -1,14 +1,16 @@ #pragma once +#include +#include namespace xrpl::Tuning { /** How many ledgers off a server can be and we will still consider it converged */ -static constexpr auto kConvergedLedgerLimit = 24; +static constexpr std::uint32_t kConvergedLedgerLimit = 24; /** How many ledgers off a server has to be before we consider it diverged */ -static constexpr auto kDivergedLedgerLimit = 128; +static constexpr std::uint32_t kDivergedLedgerLimit = 128; /** The soft cap on the number of ledger entries in a single reply. */ static constexpr auto kSoftMaxReplyNodes = 8192; From 2728e11809b3a62fc5e17af95a8f300364691c67 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 27 May 2026 01:12:30 +0100 Subject: [PATCH 007/121] fix: Set request size limits and differential pricing for get-object-by-hash calls --- src/test/overlay/TMGetObjectByHash_test.cpp | 18 +- src/xrpld/overlay/detail/PeerImp.cpp | 224 +++++++++++++++----- src/xrpld/overlay/detail/PeerImp.h | 67 ++++++ src/xrpld/overlay/detail/Tuning.h | 90 ++++++++ 4 files changed, 346 insertions(+), 53 deletions(-) diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index 961e1b7eb4..e579989181 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -100,6 +100,17 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite return lastSentMessage_; } + // Synchronous test access to the JobQueue-dispatched processor. + // The production path runs this on JtLedgerReq; tests need a + // synchronous entry point to inspect the reply via send(). + // PeerImp::processGetObjectByHash is `protected` so the derived + // test subclass can call it directly. + void + runProcessGetObjectByHash(std::shared_ptr const& m) + { + processGetObjectByHash(m); + } + static void resetId() { @@ -179,6 +190,10 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite /** * Test that reply is limited to hardMaxReplyNodes when more objects * are requested than the limit allows. + * + * `onMessage(TMGetObjectByHash)` dispatches the generic-query path + * to the JobQueue, so tests invoke the synchronous processor + * directly via `runProcessGetObjectByHash`. */ void testReplyLimit(size_t const numObjects, int const expectedReplySize) @@ -191,8 +206,7 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite auto peer = createPeer(env); auto request = createRequest(numObjects, env); - // Call the onMessage handler - peer->onMessage(request); + peer->runProcessGetObjectByHash(request); // Verify that a reply was sent auto sentMessage = peer->getLastSentMessage(); diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 324e8e6b87..822ef05304 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -81,6 +82,7 @@ #include #include +#include #include #include #include @@ -393,11 +395,21 @@ void PeerImp::charge(Resource::Charge const& fee, std::string const& context) { if ((usage_.charge(fee, context) == Resource::Disposition::Drop) && - usage_.disconnect(pJournal_) && strand_.running_in_this_thread()) + usage_.disconnect(pJournal_)) { - // Sever the connection - overlay_.incPeerDisconnectCharges(); - fail("charge: Resources"); + // Idempotent: only the first worker to observe Drop counts the + // metric and posts fail(). Without the guard, several queued + // workers can all see Drop before fail() lands on the strand, + // overcounting peerDisconnectsCharges_ and posting duplicate + // shutdowns. fail(std::string const&) self-posts to strand_ + // when invoked off-strand. + bool expected = false; + if (chargeDisconnectFired_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) + { + overlay_.incPeerDisconnectCharges(); + fail("charge: Resources"); + } } } @@ -2473,63 +2485,63 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - protocol::TMGetObjectByHash reply; - - reply.set_query(false); - - reply.set_type(packet.type()); - if (packet.has_ledgerhash()) { if (!stringIsUInt256Sized(packet.ledgerhash())) { - fee_.update(Resource::kFeeMalformedRequest, "ledger hash"); + JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_; + fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash"); return; } - - reply.set_ledgerhash(packet.ledgerhash()); } - - fee_.update(Resource::kFeeModerateBurdenPeer, " received a get object by hash request"); - - // This is a very minimal implementation - for (int i = 0; i < packet.objects_size(); ++i) + // Reject oversized requests before touching the NodeStore. + // The legitimate upper bound (InboundLedger::getNeededHashes()) + // is 8 hashes; anything beyond kHardMaxReplyNodes is non-conforming. + if (packet.objects_size() > Tuning::kHardMaxReplyNodes) { - auto const& obj = packet.objects(i); - if (obj.has_hash() && stringIsUInt256Sized(obj.hash())) - { - uint256 const hash = uint256::fromRaw(obj.hash()); - // VFALCO TODO Move this someplace more sensible so we dont - // need to inject the NodeStore interfaces. - std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; - auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)}; - if (nodeObject) - { - protocol::TMIndexedObject& newObj = *reply.add_objects(); - newObj.set_hash(hash.begin(), hash.size()); - newObj.set_data(&nodeObject->getData().front(), nodeObject->getData().size()); - - if (obj.has_nodeid()) - newObj.set_index(obj.nodeid()); - if (obj.has_ledgerseq()) - newObj.set_ledgerseq(obj.ledgerseq()); - - // Check if by adding this object, reply has reached its - // limit - if (reply.objects_size() >= Tuning::kHardMaxReplyNodes) - { - fee_.update( - Resource::kFeeModerateBurdenPeer, - "Reply limit reached. Truncating reply."); - break; - } - } - } + JLOG(pJournal_.warn()) + << "GetObj: oversized request from peer " << id_ << " (" << packet.objects_size() + << " > " << Tuning::kHardMaxReplyNodes << ")"; + fee_.update(Resource::kFeeInvalidData, "oversized get object request"); + return; } - JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " - << packet.objects_size(); - send(std::make_shared(reply, protocol::mtGET_OBJECTS)); + // Dispatch heavy synchronous NodeStore lookups off the peer's + // I/O strand and onto the bounded job queue, mirroring the pattern + // used by processLedgerRequest. + std::weak_ptr const weak = shared_from_this(); + bool const queued = app_.getJobQueue().addJob(JtLedgerReq, "RcvGetObjByHash", [weak, m]() { + auto peer = weak.lock(); + if (!peer) + return; + try + { + peer->processGetObjectByHash(m); + } + catch (std::exception const& e) + { + // Surface backend failures (NodeStore I/O, allocation) + // back through the resource model so a misbehaving peer + // is still accountable rather than silently dropped. + JLOG(peer->pJournal_.warn()) << "GetObj: handler threw: " << e.what(); + peer->charge(Resource::kFeeRequestNoReply, "get object handler exception"); + } + }); + if (!queued) + { + // The JobQueue is no longer accepting new work (typically + // because it is shutting down / has been joined). + JLOG(pJournal_.warn()) << "GetObj: job queue refused request from peer " << id_; + return; + } + + // Admission-time charge: a peer that floods enqueues would + // otherwise be billed only the trivial onMessageEnd fee per + // message until the JobQueue catches up, re-creating an + // uncharged DoS window. Charge the base burden up-front (after + // a successful enqueue); the per-lookup differential is added + // in the worker. + fee_.update(Resource::kFeeModerateBurdenPeer, "received a get object by hash request"); } else { @@ -2585,6 +2597,69 @@ PeerImp::onMessage(std::shared_ptr const& m) } } +void +PeerImp::processGetObjectByHash(std::shared_ptr const& m) +{ + protocol::TMGetObjectByHash const& packet = *m; + + protocol::TMGetObjectByHash reply; + reply.set_query(false); + reply.set_type(packet.type()); + + if (packet.has_ledgerhash()) + { + reply.set_ledgerhash(packet.ledgerhash()); + } + + // Defense in depth: caller (onMessage) already validates cheap + // structural properties of the request before dispatching here: + // - objects_size() <= kHardMaxReplyNodes (oversize gate) + // - if has_ledgerhash() then ledgerhash is uint256-sized + // The iteration cap below mirrors the oversize gate so this method + // remains safe if invoked directly by tests or future callers, and + // a peer cannot drive unbounded NodeStore lookups by sending + // non-existent hashes. + int const requested = packet.objects_size(); + int const iterLimit = std::min(requested, Tuning::kHardMaxReplyNodes); + + for (int i = 0; i < iterLimit; ++i) + { + auto const& obj = packet.objects(i); + if (!obj.has_hash() || !stringIsUInt256Sized(obj.hash())) + continue; + + uint256 const hash = uint256::fromRaw(obj.hash()); + // VFALCO TODO Move this someplace more sensible so we don't + // need to inject the NodeStore interfaces. + std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; + auto const nodeObject = app_.getNodeStore().fetchNodeObject(hash, seq); + if (!nodeObject) + continue; + + protocol::TMIndexedObject& newObj = *reply.add_objects(); + newObj.set_hash(hash.begin(), hash.size()); + auto const& data = nodeObject->getData(); + newObj.set_data(data.data(), data.size()); + if (obj.has_nodeid()) + newObj.set_index(obj.nodeid()); + if (obj.has_ledgerseq()) + newObj.set_ledgerseq(obj.ledgerseq()); + } + + // Apply work-proportional charge. `charge()` posts the disconnect + // step (if any) back to strand_, so it is safe to call from this + // JobQueue worker thread. + charge( + // We pass `requested` directly here, instead of actual lookups done. Which could be + // std::min(packet.objects_size(), static_cast(Tuning::kHardMaxReplyNodes)); + // Because we want to charge as per the request size, to discourage large requests. + computeGetObjectByHashFee(requested, reply.objects_size()), + "processed get object by hash request"); + + JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " << requested; + send(std::make_shared(reply, protocol::mtGET_OBJECTS)); +} + void PeerImp::onMessage(std::shared_ptr const& m) { @@ -3412,6 +3487,53 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) send(std::make_shared(ledgerData, protocol::mtLEDGER_DATA)); } +// Differential pricing helper. Returns only the *dynamic* component +// of the per-message charge — the base `kFeeModerateBurdenPeer` is +// applied at admission time in `onMessage(TMGetObjectByHash)` so a +// high traffic client pays for the message regardless of when (or +// whether) the worker runs. +// +// Dynamic charge model: +// +// billable = max(0, requested - kFreeObjectsPerRequest) +// missed = max(0, requested - found) +// billableMisses = min(missed, billable) // misses billed first +// billableHits = billable - billableMisses +// sizeBand = (requested > kBandMediumMax) ? kCostBandLarge +// : (requested > kBandSmallMax) ? kCostBandMedium +// : kCostBandSmall +// dynamic = billableHits * kCostPerLookupHit +// + billableMisses * kCostPerLookupMiss +// + sizeBand +// +// Misses are billed first against the billable budget because a node store +// seek dominates a cache hit and because invalid hashes are ~100% miss by construction. +Resource::Charge +PeerImp::computeGetObjectByHashFee(int const requested, int const found) +{ + int const billable = std::max(0, requested - static_cast(Tuning::kFreeObjectsPerRequest)); + // Clamp `missed` so a future caller passing found > requested cannot + // produce a negative value that flips the hits/misses split. + int const missed = std::max(0, requested - found); + int const billableMisses = std::min(missed, billable); + int const billableHits = billable - billableMisses; + + int sizeBand = Tuning::kCostBandSmall; + if (requested > Tuning::kBandMediumMax) + { + sizeBand = Tuning::kCostBandLarge; + } + else if (requested > Tuning::kBandSmallMax) + { + sizeBand = Tuning::kCostBandMedium; + } + + int const dynamic = (billableHits * Tuning::kCostPerLookupHit) + + (billableMisses * Tuning::kCostPerLookupMiss) + sizeBand; + + return Resource::Charge(dynamic, "GetObject differential"); +} + int PeerImp::getScore(bool haveItem) const { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index f5d87371be..26d7e0a832 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -147,6 +147,12 @@ private: protocol::TMStatusChange lastStatus_; Resource::Consumer usage_; ChargeWithContext fee_; + + // One-shot guard so concurrent JobQueue workers cannot double-count + // the per-connection peer-disconnect-by-charge metric (and cannot + // post duplicate fail() calls) when several queued requests cross + // kDropThreshold before the first fail() lands on the strand. + std::atomic chargeDisconnectFired_{false}; std::shared_ptr const slot_; boost::beast::multi_buffer readBuffer_; http_request_type request_; @@ -624,6 +630,67 @@ private: void processLedgerRequest(std::shared_ptr const& m); + +protected: + // Kept `protected` so test subclasses (see + // TMGetObjectByHash_test) can drive the + // synchronous processor and the differential-pricing helper without + // routing through the JobQueue or going through `friend` plumbing. + // Production callers reach these members only via + // `onMessage(TMGetObjectByHash)` → JobQueue → `processGetObjectByHash`. + + /** Process a generic-query TMGetObjectByHash message. + + Dispatched from `onMessage(TMGetObjectByHash)` to the JobQueue + (`JtLedgerReq`) so synchronous NodeStore lookups do not block the + peer's I/O strand. Caps iteration at `Tuning::kHardMaxReplyNodes` + regardless of hit/miss outcome and applies differential pricing + via `computeGetObjectByHashFee()` after the fetch loop completes. + + @param m The protocol message containing requested object hashes. + */ + void + processGetObjectByHash(std::shared_ptr const& m); + + /** Compute the per-message resource charge for a TMGetObjectByHash + request based on how much work was actually performed. + + The charge has three components on top of the base + `Resource::kFeeModerateBurdenPeer`: + - per-hit lookup cost (cheap; usually served from cache) + - per-miss lookup cost (expensive node store seeks) + - request-size band surcharge (escalates abusive batch sizes) + + The first `Tuning::kFreeObjectsPerRequest` objects are free so + that legitimate `InboundLedger::getNeededHashes()` traffic + (at most 8 objects) is unaffected. + + @param requested Number of objects requested by the message. This + value is used for request-size pricing and may + exceed `Tuning::kHardMaxReplyNodes` when this + helper is called directly, even though processing + caps the iterations to `Tuning::kHardMaxReplyNodes`. + @param found Number of objects successfully returned in the + reply. + @return A `Resource::Charge` whose cost reflects the work performed. + */ + static Resource::Charge + computeGetObjectByHashFee(int const requested, int const found); + + /** Read-only accessor for the accumulated peer-message charge. + + Exposed at `protected` scope so test subclasses can verify the + oversized-request rejection path (Layer 1) without invoking the + full JobQueue handler. Production callers should never read this back — + the value is consumed by `charge()`/`disconnect()` internally. + + @return The current `Resource::Charge` accumulated on `fee_`. + */ + Resource::Charge + currentFeeCharge() const + { + return fee_.fee; + } }; //------------------------------------------------------------------------------ diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index 97ce3f5188..20a60d470e 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -1,4 +1,6 @@ #pragma once +#include + #include #include @@ -39,4 +41,92 @@ static constexpr auto kMaxQueryDepth = 3; /** Size of buffer used to read from the socket. */ constexpr std::size_t kReadBufferBytes = 16384; +/** TMGetObjectByHash differential pricing. + + Honest peers ask for at most 8 hashes per call (the header, or up to + 4 state + 4 tx hashes from `InboundLedger::getNeededHashes()`). The + free tier covers them at zero cost. Beyond that, each lookup is billed: + 'misses' cost much more than 'hits' because a miss does a node store seek + while a hit is usually served from cache. On top of that, a size-band + surcharge kicks in for larger requests so an attacker who crams a + single message with thousands of hashes blows past + `Resource::kDropThreshold` and gets disconnected. + + The numbers below are picked to keep three things true given + `kDropThreshold = 25000`: + + - Honest traffic (<= 8 objects per request) is free. + - A single all-miss request at `kHardMaxReplyNodes` (12288) costs + more than the drop threshold, so an attacker gets dropped in one + message. + - A peer spamming 1024-object hit-only requests gets dropped in + ~19 messages — fast enough to be useful, slow enough that an + honest peer momentarily sending oversized requests has time to + back off. */ + +/** How many objects a request can ask for before per-lookup billing + begins? + Twice the honest peak (8) so a peer that occasionally retries a hash + never trips pricing. Same value as `SHAMapInnerNode::kBranchFactor`; + that's a coincidence, not a requirement. */ +static constexpr auto kFreeObjectsPerRequest = 16; + +/** Cost of one cache-hit lookup. The unit; everything else is a + multiple of this. */ +static constexpr auto kCostPerLookupHit = 1; + +/** Cost of one node-store miss, in units of `kCostPerLookupHit`. + + A miss does a node store disk seek; a hit usually comes from cache. + The 8x ratio is an order-of-magnitude guess at the latency gap on + SSD-backed nodes, not a measured number. The math only requires this + to be at least 2 — any smaller and a full-miss request at the hard + cap wouldn't trip the drop threshold. 8 leaves headroom: if + `kDropThreshold` goes up or `kHardMaxReplyNodes` comes down, the + drop-on-attack property still holds without a code change. */ +static constexpr auto kCostPerLookupMiss = 8; + +/** Size-band surcharges. Whichever band a request's size falls into, + its surcharge is added once on top of the per-lookup cost. + + The job of the surcharge is to make crossing a band edge feel like + a step, not a slope. With these values, the cost roughly doubles or triples at each cliff: + + n=64: costs 48 => n=65 costs 149 (~3x jump) + n=1024: costs 1108 => n=1025 costs 2009 (~2x jump) + + The 10x step between medium and large mirrors the ~16x step + between the band edges (64 -> 1024) so the cliff feels comparable + at both scales. + */ +static constexpr auto kCostBandSmall = 0; +static constexpr auto kCostBandMedium = 100; +static constexpr auto kCostBandLarge = 1000; + +/** How many hashes per type an honest peer asks for at a time. + + Matches the `4` passed to `neededStateHashes(4)` and + `neededTxHashes(4)` in `InboundLedger::getNeededHashes()`. Kept here + instead of imported from the ledger module so overlay stays + self-contained; if that `4` ever changes, update this in lockstep or + the band thresholds below will start charging honest peers. */ +static constexpr auto kLegitHashesPerType = 4; + +/** Cutoffs that decide which size band a request falls into. + + A SHAMap inner node has 16 children; an honest peer asks for 4 + hashes per type. So: + + kBandSmallMax = 4 * 16 = 64 // one inner node's worth + kBandMediumMax = 4 * 16^2 = 1024 // a depth-2 subtree's worth + + A request up to 64 objects is small (no surcharge); up to 1024 is + medium; anything larger is large. The bounds are inclusive: a + request of exactly 64 is small, 65 is medium. Anything past 1024 is + well beyond what the honest sync path produces, so it's billed at + the large rate to drive attack-shaped traffic over the drop + threshold quickly. */ +static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor; +static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor; + } // namespace xrpl::Tuning From e29dc474b3d3eb10bb9ff3407cc378c71800124b Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 27 May 2026 14:42:48 +0100 Subject: [PATCH 008/121] refactor: Improve payment channel closing and returned error codes --- .../ledger/helpers/PaymentChannelHelpers.h | 36 +++++++++++++++++++ .../ledger/helpers/PaymentChannelHelpers.cpp | 30 ++++++++++++++++ .../payment_channel/PaymentChannelClaim.cpp | 28 +++++++++------ .../payment_channel/PaymentChannelFund.cpp | 34 +++++++++++------- src/test/app/PayChan_test.cpp | 5 ++- 5 files changed, 109 insertions(+), 24 deletions(-) diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 24838f1331..4528431028 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -5,8 +5,21 @@ #include #include +#include +#include +#include + namespace xrpl { +/** Close a payment channel and return its remaining funds to the channel owner. + * + * @param slep The SLE for the PayChannel object to close. + * @param view The apply view in which ledger state modifications are made. + * @param key The ledger key identifying the PayChannel entry. + * @param j Journal used for fatal-level diagnostic messages. + * @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal + * fails; tefINTERNAL if the source account SLE cannot be found. + */ TER closeChannel( std::shared_ptr const& slep, @@ -14,4 +27,27 @@ closeChannel( uint256 const& key, beast::Journal j); +/** Add two uint32_t values with saturation at UINT32_MAX. + * + * @param rules The current ledger rules used to check amendment status. + * @param lhs Left-hand operand. + * @param rhs Right-hand operand. + * @return @p lhs + @p rhs, saturated at UINT32_MAX when the amendment + * is active. + */ +uint32_t +saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs); + +/** Determine whether a payment channel time field represents an expired time. + * + * @param view The apply view providing the parent close time and rules. + * @param timeField The optional expiry timestamp (seconds since the XRP + * Ledger epoch). If empty, the function returns false. + * @return @c true if @p timeField is set and the indicated time is + * in the past relative to the view's parent close time; + * @c false otherwise. + */ +bool +isChannelExpired(ApplyView const& view, std::optional timeField); + } // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index 31c206d85b..ba5ce50989 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -5,14 +5,20 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include +#include +#include #include +#include namespace xrpl { @@ -65,4 +71,28 @@ closeChannel( return tesSUCCESS; } +uint32_t +saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs) +{ + if (rules.enabled(fixCleanup3_2_0)) + { + static constexpr auto kUint32Max = + static_cast(std::numeric_limits::max()); + uint64_t const saturatedResult = std::min(uint64_t{lhs} + rhs, kUint32Max); + return static_cast(saturatedResult); + } + + return lhs + rhs; +} + +bool +isChannelExpired(ApplyView const& view, std::optional timeField) +{ + if (!timeField) + return false; + if (view.rules().enabled(fixCleanup3_2_0)) + return after(view.header().parentCloseTime, *timeField); + return view.header().parentCloseTime.time_since_epoch().count() >= *timeField; +} + } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index cc99b8f62d..6a84d44746 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -43,6 +43,9 @@ PaymentChannelClaim::getFlagsMask(PreflightContext const&) NotTEC PaymentChannelClaim::preflight(PreflightContext const& ctx) { + if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero) + return temMALFORMED; + auto const bal = ctx.tx[~sfBalance]; if (bal && (!isXRP(*bal) || *bal <= beast::kZero)) return temBAD_AMOUNT; @@ -117,12 +120,10 @@ PaymentChannelClaim::doApply() AccountID const txAccount = ctx_.tx[sfAccount]; auto const curExpiration = (*slep)[~sfExpiration]; + if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) || + isChannelExpired(ctx_.view(), curExpiration)) { - auto const cancelAfter = (*slep)[~sfCancelAfter]; - auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); - if ((cancelAfter && closeTime >= *cancelAfter) || - (curExpiration && closeTime >= *curExpiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); } if (txAccount != src && txAccount != dst) @@ -135,13 +136,19 @@ PaymentChannelClaim::doApply() auto const reqBalance = ctx_.tx[sfBalance].xrp(); if (txAccount == dst && !ctx_.tx[~sfSignature]) - return temBAD_SIGNATURE; + { + return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} + : TER{temBAD_SIGNATURE}; + } if (ctx_.tx[~sfSignature]) { PublicKey const pk((*slep)[sfPublicKey]); if (ctx_.tx[sfPublicKey] != pk) - return temBAD_SIGNER; + { + return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} + : TER{temBAD_SIGNER}; + } } if (reqBalance > chanFunds) @@ -185,9 +192,10 @@ PaymentChannelClaim::doApply() if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); - auto const settleExpiration = - ctx_.view().header().parentCloseTime.time_since_epoch().count() + - (*slep)[sfSettleDelay]; + auto const settleExpiration = saturatingAdd( + ctx_.view().rules(), + ctx_.view().header().parentCloseTime.time_since_epoch().count(), + (*slep)[sfSettleDelay]); if (!curExpiration || *curExpiration > settleExpiration) { diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 41906aa3da..dcd2797dd1 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,9 @@ PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx) NotTEC PaymentChannelFund::preflight(PreflightContext const& ctx) { + if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero) + return temMALFORMED; + if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero)) return temBAD_AMOUNT; @@ -47,13 +51,12 @@ PaymentChannelFund::doApply() AccountID const src = (*slep)[sfAccount]; auto const txAccount = ctx_.tx[sfAccount]; - auto const expiration = (*slep)[~sfExpiration]; + auto const curExpiration = (*slep)[~sfExpiration]; + if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) || + isChannelExpired(ctx_.view(), curExpiration)) { - auto const cancelAfter = (*slep)[~sfCancelAfter]; - auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); - if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); } if (src != txAccount) @@ -62,16 +65,21 @@ PaymentChannelFund::doApply() return tecNO_PERMISSION; } - if (auto extend = ctx_.tx[~sfExpiration]) + if (auto newExpiration = ctx_.tx[~sfExpiration]) { - auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() + - (*slep)[sfSettleDelay]; - if (expiration && *expiration < minExpiration) - minExpiration = *expiration; + auto minExpiration = saturatingAdd( + ctx_.view().rules(), + ctx_.view().header().parentCloseTime.time_since_epoch().count(), + (*slep)[sfSettleDelay]); + if (curExpiration && *curExpiration < minExpiration) + minExpiration = *curExpiration; - if (*extend < minExpiration) - return temBAD_EXPIRATION; - (*slep)[~sfExpiration] = *extend; + if (*newExpiration < minExpiration) + { + return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} + : TER{temBAD_EXPIRATION}; + } + (*slep)[~sfExpiration] = *newExpiration; ctx_.view().update(slep); } diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index b81afa830e..1e465aad91 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -1992,7 +1992,10 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{testableAmendments()}; + // fixCleanup3_2_0 changes payment-channel error codes (tem* -> tec*) + // and channel-closing semantics. This suite asserts the + // pre-amendment behavior, so run it with the amendment disabled. + FeatureBitset const all{testableAmendments() - fixCleanup3_2_0}; testWithFeats(all); testDepositAuthCreds(); testMetaAndOwnership(all - fixIncludeKeyletFields); From f98c251011e606e13a562598f2ee974d0a35b624 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 27 May 2026 16:18:18 +0100 Subject: [PATCH 009/121] refactor: Improve tracking of book (un)subscriptions --- .../scripts/levelization/results/ordering.txt | 2 - include/xrpl/ledger/BookListeners.h | 49 ----- include/xrpl/ledger/OrderBookDB.h | 50 ++--- include/xrpl/server/InfoSub.h | 84 ++++++++- include/xrpl/server/NetworkOPs.h | 13 ++ src/libxrpl/ledger/BookListeners.cpp | 55 ------ src/libxrpl/server/InfoSub.cpp | 87 +++++++-- src/xrpld/app/ledger/OrderBookDBImpl.cpp | 82 ++------ src/xrpld/app/ledger/OrderBookDBImpl.h | 19 -- src/xrpld/app/misc/NetworkOPs.cpp | 176 ++++++++++++++++-- .../rpc/handlers/subscribe/Unsubscribe.cpp | 14 +- 11 files changed, 384 insertions(+), 247 deletions(-) delete mode 100644 include/xrpl/ledger/BookListeners.h delete mode 100644 src/libxrpl/ledger/BookListeners.cpp diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index c2000d1768..7a5be869a5 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -12,7 +12,6 @@ libxrpl.ledger > xrpl.json libxrpl.ledger > xrpl.ledger libxrpl.ledger > xrpl.nodestore libxrpl.ledger > xrpl.protocol -libxrpl.ledger > xrpl.server libxrpl.ledger > xrpl.shamap libxrpl.net > xrpl.basics libxrpl.net > xrpl.net @@ -206,7 +205,6 @@ xrpl.core > xrpl.protocol xrpl.json > xrpl.basics xrpl.ledger > xrpl.basics xrpl.ledger > xrpl.protocol -xrpl.ledger > xrpl.server xrpl.ledger > xrpl.shamap xrpl.net > xrpl.basics xrpl.nodestore > xrpl.basics diff --git a/include/xrpl/ledger/BookListeners.h b/include/xrpl/ledger/BookListeners.h deleted file mode 100644 index 3b96aca680..0000000000 --- a/include/xrpl/ledger/BookListeners.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -namespace xrpl { - -/** Listen to public/subscribe messages from a book. */ -class BookListeners -{ -public: - using pointer = std::shared_ptr; - - BookListeners() = default; - - /** Add a new subscription for this book - */ - void - addSubscriber(InfoSub::ref sub); - - /** Stop publishing to a subscriber - */ - void - removeSubscriber(std::uint64_t sub); - - /** Publish a transaction to subscribers - - Publish a transaction to clients subscribed to changes on this book. - Uses havePublished to prevent sending duplicate transactions to clients - that have subscribed to multiple books. - - @param jvObj JSON transaction data to publish - @param havePublished InfoSub sequence numbers that have already - published this transaction. - - */ - void - publish(MultiApiJson const& jvObj, hash_set& havePublished); - -private: - std::recursive_mutex lock_; - - hash_map listeners_; -}; - -} // namespace xrpl diff --git a/include/xrpl/ledger/OrderBookDB.h b/include/xrpl/ledger/OrderBookDB.h index a0aee58e2a..a44183900c 100644 --- a/include/xrpl/ledger/OrderBookDB.h +++ b/include/xrpl/ledger/OrderBookDB.h @@ -1,11 +1,11 @@ #pragma once +#include +#include #include -#include #include #include #include -#include #include #include @@ -77,34 +77,24 @@ public: */ virtual bool isBookToXRP(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - - /** - * Process a transaction for order book tracking. - * @param ledger The ledger the transaction was applied to - * @param alTx The transaction to process - * @param jvObj The JSON object of the transaction - */ - virtual void - processTxn( - std::shared_ptr const& ledger, - AcceptedLedgerTx const& alTx, - MultiApiJson const& jvObj) = 0; - - /** - * Get the book listeners for a book. - * @param book The book to get the listeners for - * @return The book listeners for the book - */ - virtual BookListeners::pointer - getBookListeners(Book const&) = 0; - - /** - * Create a new book listeners for a book. - * @param book The book to create the listeners for - * @return The new book listeners for the book - */ - virtual BookListeners::pointer - makeBookListeners(Book const&) = 0; }; +/** Extract the set of books affected by a transaction. + * + * Walks the transaction's metadata nodes and collects every order book + * whose offers were created, modified, or deleted. Used by NetworkOPs to + * fan transaction notifications out to book subscribers. + * + * @param alTx The accepted ledger transaction to inspect. + * @param j Journal used to log per-node parsing failures. Inspecting an + * offer node can throw if a required field is missing; in that + * case the bad node is skipped and a warn-level message is + * emitted via @p j. Other affected books in the same transaction + * are still returned. + * @return The set of books whose offers were created, modified, or + * deleted. May be empty for non-offer transactions. + */ +hash_set +affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j); + } // namespace xrpl diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index e93676a938..f316885fd6 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,19 @@ public: }; /** Manages a client's subscription to data feeds. + * + * An InfoSub holds a non-owning reference to its `Source` (typically the + * process-wide `NetworkOPsImp`). The destructor reaches back into the + * `Source` to remove this subscriber from every server-side subscription + * map. + * + * @note Lifetime contract: every `InfoSub` instance MUST be destroyed + * before the backing `Source`. NetworkOPsImp shutdown drops all + * subscriber strong refs before its own teardown to satisfy this. + * @note Thread-safety: per-instance state is guarded by `lock_`. The + * destructor reads tracking sets without taking `lock_` because + * the strong-pointer ref-count is zero at destruction time, so + * no other thread can be calling the public mutators. */ class InfoSub : public CountedObject { @@ -117,8 +131,43 @@ public: virtual bool subBook(ref ispListener, Book const&) = 0; + + /** + * Remove a book subscription for a live subscriber. + * + * Clears the book from the subscriber's own tracking set + * (InfoSub::bookSubscriptions_) and then removes the server-side + * entry from subBook_. Call this from RPC unsubscribe handlers. + * + * @param ispListener The subscriber requesting removal. + * @param book The order book to unsubscribe from. + * @return true if the entry was present and removed, false if the + * subscriber was not subscribed to @p book. + * + * @note Thread-safety: acquires subLock_ internally. + * @note Do NOT call from ~InfoSub(). Use unsubBookInternal instead + * to avoid a redundant write-back to bookSubscriptions_ on a + * partially-destroyed object. + */ virtual bool - unsubBook(std::uint64_t uListener, Book const&) = 0; + unsubBook(ref ispListener, Book const&) = 0; + + /** + * Remove a book subscription during InfoSub teardown. + * + * Removes only the server-side entry from subBook_. Does NOT touch + * InfoSub::bookSubscriptions_ because the InfoSub is being destroyed. + * Called by ~InfoSub() for each book in bookSubscriptions_. + * + * @param uListener The sequence number of the subscriber being torn down. + * @param book The order book entry to remove. + * @return true if the entry was present and removed, false otherwise + * (e.g., already removed by a concurrent RPC unsubscribe). + * + * @note Thread-safety: acquires subLock_ internally. + */ + virtual bool + unsubBookInternal(std::uint64_t uListener, Book const&) = 0; virtual bool subTransactions(ref ispListener) = 0; @@ -158,6 +207,13 @@ public: addRpcSub(std::string const& strUrl, ref rspEntry) = 0; virtual bool tryRemoveRpcSub(std::string const& strUrl) = 0; + + /** Journal used by InfoSub for diagnostics that occur after the + * owning subsystem (e.g. application-level Logs) is the only + * surviving sink — primarily destructor-time cleanup failures. + */ + [[nodiscard]] virtual beast::Journal const& + journal() const = 0; }; public: @@ -184,6 +240,31 @@ public: void deleteSubAccountInfo(AccountID const& account, bool rt); + /** Record that this subscriber is following @p book. + * + * Called by NetworkOPsImp::subBook so that ~InfoSub() can issue a + * matching unsubBook for every book this subscriber is tracking, + * keeping per-subscriber state symmetric with the server-side map. + * + * @param book The order book this subscriber has just subscribed to. + * @note Idempotent: re-inserting an already-tracked book is a no-op. + * @note Thread-safe: takes InfoSub::lock_. + */ + void + insertBookSubscription(Book const& book); + + /** Stop tracking @p book for this subscriber. + * + * Called by the unsubscribe RPC handler so that the book is not + * re-unsubscribed by ~InfoSub(). Pairs with insertBookSubscription. + * + * @param book The order book to forget. + * @note No-op if @p book was not previously inserted. + * @note Thread-safe: takes InfoSub::lock_. + */ + void + deleteBookSubscription(Book const& book); + // return false if already subscribed to this account bool insertSubAccountHistory(AccountID const& account); @@ -217,6 +298,7 @@ private: std::shared_ptr request_; std::uint64_t seq_; hash_set accountHistorySubscriptions_; + hash_set bookSubscriptions_; unsigned int apiVersion_ = 0; static int diff --git a/include/xrpl/server/NetworkOPs.h b/include/xrpl/server/NetworkOPs.h index e2aa17566e..785d808935 100644 --- a/include/xrpl/server/NetworkOPs.h +++ b/include/xrpl/server/NetworkOPs.h @@ -249,6 +249,19 @@ public: virtual void stateAccounting(json::Value& obj) = 0; + + /** Total number of (book, subscriber) entries currently tracked. + * + * Counts every weak_ptr stored across every book in subBook_, NOT the + * number of distinct subscribers and NOT the number of distinct + * books: a single subscriber following N books contributes N entries. + * + * @note Diagnostic accessor; intended for tests and operator visibility + * into per-book subscription state. The returned value is a + * snapshot under the subscription lock. + */ + virtual std::size_t + getBookSubscribersCount() = 0; }; } // namespace xrpl diff --git a/src/libxrpl/ledger/BookListeners.cpp b/src/libxrpl/ledger/BookListeners.cpp deleted file mode 100644 index d78da4c73e..0000000000 --- a/src/libxrpl/ledger/BookListeners.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include - -#include -#include -#include -#include - -#include -#include - -namespace xrpl { - -void -BookListeners::addSubscriber(InfoSub::ref sub) -{ - std::scoped_lock const sl(lock_); - listeners_[sub->getSeq()] = sub; -} - -void -BookListeners::removeSubscriber(std::uint64_t seq) -{ - std::scoped_lock const sl(lock_); - listeners_.erase(seq); -} - -void -BookListeners::publish(MultiApiJson const& jvObj, hash_set& havePublished) -{ - std::scoped_lock const sl(lock_); - auto it = listeners_.cbegin(); - - while (it != listeners_.cend()) - { - InfoSub::pointer p = it->second.lock(); - - if (p) - { - // Only publish jvObj if this is the first occurrence - if (havePublished.emplace(p->getSeq()).second) - { - jvObj.visit( - p->getApiVersion(), // - [&](json::Value const& jv) { p->send(jv, true); }); - } - ++it; - } - else - { - it = listeners_.erase(it); - } - } -} - -} // namespace xrpl diff --git a/src/libxrpl/server/InfoSub.cpp b/src/libxrpl/server/InfoSub.cpp index 87b48296a1..353c295856 100644 --- a/src/libxrpl/server/InfoSub.cpp +++ b/src/libxrpl/server/InfoSub.cpp @@ -1,15 +1,47 @@ #include +#include +#include #include #include +#include #include #include +#include #include #include namespace xrpl { +namespace { + +// Wraps a Source teardown call so that an exception from one cleanup +// step does not prevent the subsequent steps from running. Source methods +// acquire a lock and can throw std::system_error; a throw out of ~InfoSub +// during stack unwinding would terminate the process. Failures are +// reported through the Source's Journal so they reach the configured log +// sinks; JLOG itself cannot throw, so the noexcept guarantee holds. +template +void +safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept +{ + try + { + f(); + } + catch (std::exception const& e) + { + JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: " << e.what(); + } + catch (...) + { + JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: unknown exception"; + } +} + +} // namespace + // This is the primary interface into the "client" portion of the program. // Code that wants to do normal operations on the network such as // creating and monitoring accounts, creating transactions, and so on @@ -32,25 +64,44 @@ InfoSub::InfoSub(Source& source, Consumer consumer) InfoSub::~InfoSub() { - source_.unsubTransactions(seq_); - source_.unsubRTTransactions(seq_); - source_.unsubLedger(seq_); - source_.unsubManifests(seq_); - source_.unsubServer(seq_); - source_.unsubValidations(seq_); - source_.unsubPeerStatus(seq_); - source_.unsubConsensus(seq_); + // Each Source teardown call below acquires a server-side lock and + // can throw. Wrap each independent call so partial failure does not + // skip the remaining teardown steps. + + auto const& j = source_.journal(); + + safeUnsub(seq_, [&] { source_.unsubTransactions(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubRTTransactions(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubLedger(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubManifests(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubServer(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubValidations(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j); // Use the internal unsubscribe so that it won't call // back to us and modify its own parameter if (!realTimeSubscriptions_.empty()) - source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); + { + safeUnsub( + seq_, [&] { source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); }, j); + } if (!normalSubscriptions_.empty()) - source_.unsubAccountInternal(seq_, normalSubscriptions_, false); + { + safeUnsub( + seq_, [&] { source_.unsubAccountInternal(seq_, normalSubscriptions_, false); }, j); + } for (auto const& account : accountHistorySubscriptions_) - source_.unsubAccountHistoryInternal(seq_, account, false); + { + safeUnsub(seq_, [&] { source_.unsubAccountHistoryInternal(seq_, account, false); }, j); + } + + for (auto const& book : bookSubscriptions_) + { + safeUnsub(seq_, [&] { source_.unsubBookInternal(seq_, book); }, j); + } } Resource::Consumer& @@ -114,6 +165,20 @@ InfoSub::deleteSubAccountHistory(AccountID const& account) accountHistorySubscriptions_.erase(account); } +void +InfoSub::insertBookSubscription(Book const& book) +{ + std::scoped_lock const sl(lock_); + bookSubscriptions_.insert(book); +} + +void +InfoSub::deleteBookSubscription(Book const& book) +{ + std::scoped_lock const sl(lock_); + bookSubscriptions_.erase(book); +} + void InfoSub::clearRequest() { diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.cpp b/src/xrpld/app/ledger/OrderBookDBImpl.cpp index 658ec4ea7a..1e474ef949 100644 --- a/src/xrpld/app/ledger/OrderBookDBImpl.cpp +++ b/src/xrpld/app/ledger/OrderBookDBImpl.cpp @@ -9,19 +9,17 @@ #include #include #include -#include #include +#include #include #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -307,55 +305,10 @@ OrderBookDBImpl::isBookToXRP(Asset const& asset, std::optional const& do return xrpBooks_.contains(asset); } -BookListeners::pointer -OrderBookDBImpl::makeBookListeners(Book const& book) +hash_set +affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j) { - std::scoped_lock const sl(lock_); - auto ret = getBookListeners(book); - - if (!ret) - { - ret = std::make_shared(); - - listeners_[book] = ret; - XRPL_ASSERT( - getBookListeners(book) == ret, - "xrpl::OrderBookDB::makeBookListeners : result roundtrip " - "lookup"); - } - - return ret; -} - -BookListeners::pointer -OrderBookDBImpl::getBookListeners(Book const& book) -{ - BookListeners::pointer ret; - std::scoped_lock const sl(lock_); - - auto it0 = listeners_.find(book); - if (it0 != listeners_.end()) - ret = it0->second; - - return ret; -} - -// Based on the meta, send the meta to the streams that are listening. -// We need to determine which streams a given meta effects. -void -OrderBookDBImpl::processTxn( - std::shared_ptr const& ledger, - AcceptedLedgerTx const& alTx, - MultiApiJson const& jvObj) -{ - std::scoped_lock const sl(lock_); - - // For this particular transaction, maintain the set of unique - // subscriptions that have already published it. This prevents sending - // the transaction multiple times if it touches multiple ltOFFER - // entries for the same book, or if it touches multiple books and a - // single client has subscribed to those books. - hash_set havePublished; + hash_set result; for (auto const& node : alTx.getMeta().getNodes()) { @@ -363,40 +316,41 @@ OrderBookDBImpl::processTxn( { if (node.getFieldU16(sfLedgerEntryType) == ltOFFER) { - auto process = [&, this](SField const& field) { + auto extract = [&](SField const& field) { if (auto data = dynamic_cast(node.peekAtPField(field)); data && data->isFieldPresent(sfTakerPays) && data->isFieldPresent(sfTakerGets)) { - auto listeners = getBookListeners( - {data->getFieldAmount(sfTakerGets).asset(), - data->getFieldAmount(sfTakerPays).asset(), - (*data)[~sfDomainID]}); - if (listeners) - listeners->publish(jvObj, havePublished); + result.emplace( + data->getFieldAmount(sfTakerGets).asset(), + data->getFieldAmount(sfTakerPays).asset(), + (*data)[~sfDomainID]); } }; - // We need a field that contains the TakerGets and TakerPays - // parameters. if (node.getFName() == sfModifiedNode) { - process(sfPreviousFields); + extract(sfPreviousFields); } else if (node.getFName() == sfCreatedNode) { - process(sfNewFields); + extract(sfNewFields); } else if (node.getFName() == sfDeletedNode) { - process(sfFinalFields); + extract(sfFinalFields); } } } catch (std::exception const& ex) { - JLOG(j_.info()) << "processTxn: field not found (" << ex.what() << ")"; + // The bad node is skipped; other affected books in the same + // transaction are still returned. Logged at warn so a malformed + // offer node is visible to operators. + JLOG(j.warn()) << "affectedBooks: skipping malformed node (" << ex.what() << ")"; } } + + return result; } } // namespace xrpl diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.h b/src/xrpld/app/ledger/OrderBookDBImpl.h index a50f512441..a68f63c043 100644 --- a/src/xrpld/app/ledger/OrderBookDBImpl.h +++ b/src/xrpld/app/ledger/OrderBookDBImpl.h @@ -1,10 +1,7 @@ #pragma once #include -#include -#include #include -#include #include #include @@ -54,18 +51,6 @@ public: void update(std::shared_ptr const& ledger); - // see if this txn effects any orderbook - void - processTxn( - std::shared_ptr const& ledger, - AcceptedLedgerTx const& alTx, - MultiApiJson const& jvObj) override; - - BookListeners::pointer - getBookListeners(Book const&) override; - BookListeners::pointer - makeBookListeners(Book const&) override; - private: std::reference_wrapper registry_; int const pathSearchMax_; @@ -84,10 +69,6 @@ private: std::recursive_mutex lock_; - using BookToListenersMap = hash_map; - - BookToListenersMap listeners_; - std::atomic seq_; beast::Journal const j_; diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 12c79b821c..e59840befc 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -527,6 +527,8 @@ public: updateLocalTx(ReadView const& view) override; std::size_t getLocalTxCount() override; + std::size_t + getBookSubscribersCount() override; // // Monitoring: publisher side. @@ -586,7 +588,9 @@ public: bool subBook(InfoSub::ref ispListener, Book const&) override; bool - unsubBook(std::uint64_t uListener, Book const&) override; + unsubBook(InfoSub::ref ispListener, Book const&) override; + bool + unsubBookInternal(std::uint64_t uListener, Book const&) override; bool subManifests(InfoSub::ref ispListener) override; @@ -629,6 +633,12 @@ public: bool tryRemoveRpcSub(std::string const& strUrl) override; + beast::Journal const& + journal() const override + { + return journal_; + } + void stop() override { @@ -705,6 +715,32 @@ private: AcceptedLedgerTx const& transaction, bool last); + /** + * Fan transaction notifications out to all book subscribers. + * + * Extracts the set of order books affected by @p transaction, then + * delivers @p jvObj to every live subscriber of those books. + * + * Uses a two-pass design to keep subLock_ hold time short: + * 1. Under subLock_, collect strong InfoSub pointers for all live + * subscribers and prune any expired weak_ptrs encountered. + * 2. Release subLock_, then call send() on each collected pointer. + * + * @param transaction The accepted ledger transaction to inspect. + * @param jvObj JSON representation of the transaction to deliver. + * + * @note Thread-safety: acquires subLock_ for the collection pass only. + * send() is intentionally called outside the lock to avoid blocking + * all other sub/unsub/publish paths while I/O is in progress. + * @note Contention: subLock_ is shared with all other subscription types. + * On high-throughput nodes processing multi-hop payments that touch + * many offer nodes, this pass holds subLock_ longer than the old + * per-book BookListeners locks did. This is an accepted trade-off + * for lock-domain simplicity. + */ + void + pubBookTransaction(AcceptedLedgerTx const& transaction, MultiApiJson const& jvObj); + void pubProposedAccountTransaction( std::shared_ptr const& ledger, @@ -802,8 +838,19 @@ private: LedgerMaster& ledgerMaster_; + /** Maps each order book to its current set of subscribers. + * Outer key: the Book (currency pair + optional domain). + * Inner key: InfoSub::seq (unique per connection). + * Inner value: weak_ptr so that a dropped connection does not prevent + * the InfoSub from being destroyed; expired entries are pruned lazily + * by pubBookTransaction and eagerly by unsubBookInternal (~InfoSub path). + * Guarded by subLock_. + */ + using SubBookMapType = hash_map; + SubInfoMapType subAccount_; SubInfoMapType subRTAccount_; + SubBookMapType subBook_; ///< Guarded by subLock_. subRpcMapType rpcSubMap_; @@ -3191,6 +3238,16 @@ NetworkOPsImp::getLocalTxCount() return localTX_->size(); } +std::size_t +NetworkOPsImp::getBookSubscribersCount() +{ + std::scoped_lock const sl(subLock_); + std::size_t total = 0; + for (auto const& [_, subs] : subBook_) + total += subs.size(); + return total; +} + // This routine should only be used to publish accepted or validated // transactions. MultiApiJson @@ -3352,11 +3409,89 @@ NetworkOPsImp::pubValidatedTransaction( } if (transaction.getResult() == tesSUCCESS) - registry_.get().getOrderBookDB().processTxn(ledger, transaction, jvObj); + pubBookTransaction(transaction, jvObj); pubAccountTransaction(ledger, transaction, last); } +void +NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson const& jvObj) +{ + auto const books = affectedBooks(alTx, journal_); + if (books.empty()) + return; + + // Two-pass design: + // + // 1. Under subLock_, walk subBook_, collect a strong pointer for each + // unique listener (and prune any expired weak_ptrs we encounter). + // 2. Release subLock_, then send to each collected listener. + // + // Reasoning: + // * send() can be slow / blocking, so holding subLock_ across it would + // stall every other sub/unsub/pub path on this server (see the matching + // TODO above pubServer at line ~2275). + // * A strong pointer destructed while subLock_ is held risks running + // ~InfoSub() in-line, which re-enters unsubBook() and mutates the very + // subBook_/SubMapType being iterated -> dangling iterator UB. + // + // Releasing subLock_ before any InfoSub::pointer can decay solves both. + // ~InfoSub() reacquires subLock_ via unsubBook() on its own and serializes + // safely with concurrent traffic. + + std::vector listeners; + hash_set seen; + + // Sized for the common case where every affected book has at most + // one subscriber. Multi-subscriber books trigger reallocation, but + // that is rare and the upper-bound estimate (sum of per-book sizes) + // would itself require walking subBook_ twice. + listeners.reserve(books.size()); + seen.reserve(books.size()); + + { + std::scoped_lock const sl(subLock_); + + for (auto const& book : books) + { + auto it = subBook_.find(book); + if (it == subBook_.end()) + continue; + + for (auto sit = it->second.begin(); sit != it->second.end();) + { + if (auto p = sit->second.lock()) + { + // Defensive: subBook_ entries are normally cleared by + // ~InfoSub() -> unsubBook(), so we rarely see expired + // weak_ptrs here. The else branch covers the narrow race + // where the last strong ref is dropped between insertion + // and our lock() call. + if (seen.emplace(p->getSeq()).second) + listeners.emplace_back(std::move(p)); + ++sit; + } + else + { + JLOG(journal_.debug()) + << "pubBookTransaction: pruning expired weak_ptr for seq=" << sit->first; + sit = it->second.erase(sit); + } + } + + if (it->second.empty()) + subBook_.erase(it); + } + } + + for (auto const& p : listeners) + { + jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); }); + } + // listeners destructs here, outside subLock_; ~InfoSub (if any fires) + // will reacquire subLock_ via unsubBook with no iterator hazard. +} + void NetworkOPsImp::pubAccountTransaction( std::shared_ptr const& ledger, @@ -4010,26 +4145,39 @@ NetworkOPsImp::unsubAccountHistoryInternal( bool NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book) { - if (auto listeners = registry_.get().getOrderBookDB().makeBookListeners(book)) + // Server-side insert first, then InfoSub bookkeeping. If the InfoSub-side + // insert throws, the orphan in subBook_ is cleared by the expired-weak_ptr + // prune in pubBookTransaction. With the reverse ordering, ~InfoSub would + // call unsubBookInternal for a key that was never inserted server-side. { - listeners->addSubscriber(isrListener); - } - else - { - // LCOV_EXCL_START - UNREACHABLE("xrpl::NetworkOPsImp::subBook : null book listeners"); - // LCOV_EXCL_STOP + std::scoped_lock const sl(subLock_); + subBook_[book].try_emplace(isrListener->getSeq(), isrListener); } + isrListener->insertBookSubscription(book); return true; } bool -NetworkOPsImp::unsubBook(std::uint64_t uSeq, Book const& book) +NetworkOPsImp::unsubBook(InfoSub::ref isrListener, Book const& book) { - if (auto listeners = registry_.get().getOrderBookDB().getBookListeners(book)) - listeners->removeSubscriber(uSeq); + // Mirrors unsubAccount: clear the per-subscriber tracking set first so + // ~InfoSub does not re-issue an unsubBookInternal for a book the caller + // already removed, then erase the server-side entry. + isrListener->deleteBookSubscription(book); + return unsubBookInternal(isrListener->getSeq(), book); +} - return true; +bool +NetworkOPsImp::unsubBookInternal(std::uint64_t uSeq, Book const& book) +{ + std::scoped_lock const sl(subLock_); + auto it = subBook_.find(book); + if (it == subBook_.end()) + return false; + bool const erased = it->second.erase(uSeq) != 0u; + if (it->second.empty()) + subBook_.erase(it); + return erased; } std::uint32_t diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp index 36dae615b3..af42af2a55 100644 --- a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp @@ -186,13 +186,23 @@ doUnsubscribe(RPC::JsonContext& context) book.domain = domain; } - context.netOps.unsubBook(ispSub->getSeq(), book); + if (!context.netOps.unsubBook(ispSub, book)) + { + JLOG(context.j.debug()) + << "doUnsubscribe: book not subscribed (no-op for seq=" << ispSub->getSeq() + << ")"; + } // both_sides is deprecated. if ((jv.isMember(jss::both) && jv[jss::both].asBool()) || (jv.isMember(jss::both_sides) && jv[jss::both_sides].asBool())) { - context.netOps.unsubBook(ispSub->getSeq(), reversed(book)); + if (!context.netOps.unsubBook(ispSub, reversed(book))) + { + JLOG(context.j.debug()) + << "doUnsubscribe: reversed book not subscribed (no-op for seq=" + << ispSub->getSeq() << ")"; + } } } } From 82ee5b7556456cf9f9b78bed91d24e1b72eeea50 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 1 Jun 2026 16:04:45 -0400 Subject: [PATCH 010/121] refactor: Handle int and uint API versions separately --- include/xrpl/protocol/ApiVersion.h | 33 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index 345049b377..10b7571641 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -102,25 +102,32 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled) json::Value const maxVersion( betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion); - if (jv.isObject()) + if (!jv.isObject() || !jv.isMember(jss::api_version)) + return RPC::kApiVersionIfUnspecified; + + try { - if (jv.isMember(jss::api_version)) + auto const& rawVersion = jv[jss::api_version]; + switch (rawVersion.type()) { - auto const specifiedVersion = jv[jss::api_version]; - if (!specifiedVersion.isInt() && !specifiedVersion.isUInt()) - { - return RPC::kApiInvalidVersion; + case json::ValueType::Int: + if (rawVersion.asInt() < 0) + return RPC::kApiInvalidVersion; + [[fallthrough]]; + case json::ValueType::UInt: { + auto const apiVersion = rawVersion.asUInt(); + if (apiVersion < kMinVersion || apiVersion > maxVersion) + return RPC::kApiInvalidVersion; + return apiVersion; } - auto const specifiedVersionInt = specifiedVersion.asInt(); - if (specifiedVersionInt < kMinVersion || specifiedVersionInt > maxVersion) - { + default: return RPC::kApiInvalidVersion; - } - return specifiedVersionInt; } } - - return RPC::kApiVersionIfUnspecified; + catch (...) + { + return RPC::kApiInvalidVersion; + } } } // namespace RPC From 5a25c9188bd5a6ed6aafbe5e8c3be25d53da5ad3 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 1 Jun 2026 16:53:43 -0400 Subject: [PATCH 011/121] release: Bump version to 3.2.0-rc4 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 7de1862dfc..e81be00920 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0-rc3" +char const* const versionString = "3.2.0-rc4" // clang-format on ; From 47b06ecd17c3b9f2852ebd15987a85401965117e Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 3 Jun 2026 15:32:43 +0100 Subject: [PATCH 012/121] refactor: Use rocksdb includes only when it is available --- include/xrpl/basics/rocksdb.h | 29 ------------------- src/libxrpl/nodestore/ManagerImp.cpp | 4 +++ .../nodestore/backend/RocksDBFactory.cpp | 26 ++++++++--------- src/test/nodestore/import_test.cpp | 11 +++++-- 4 files changed, 24 insertions(+), 46 deletions(-) delete mode 100644 include/xrpl/basics/rocksdb.h diff --git a/include/xrpl/basics/rocksdb.h b/include/xrpl/basics/rocksdb.h deleted file mode 100644 index 3d468b0f1b..0000000000 --- a/include/xrpl/basics/rocksdb.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#if XRPL_ROCKSDB_AVAILABLE -// #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#endif diff --git a/src/libxrpl/nodestore/ManagerImp.cpp b/src/libxrpl/nodestore/ManagerImp.cpp index 5cefbfd357..1d87ff9199 100644 --- a/src/libxrpl/nodestore/ManagerImp.cpp +++ b/src/libxrpl/nodestore/ManagerImp.cpp @@ -44,8 +44,10 @@ ManagerImp::missingBackend() // the Factory classes is an undefined behaviour. void registerNuDBFactory(Manager& manager); +#if XRPL_ROCKSDB_AVAILABLE void registerRocksDBFactory(Manager& manager); +#endif void registerNullFactory(Manager& manager); void @@ -54,7 +56,9 @@ registerMemoryFactory(Manager& manager); ManagerImp::ManagerImp() { registerNuDBFactory(*this); +#if XRPL_ROCKSDB_AVAILABLE registerRocksDBFactory(*this); +#endif registerNullFactory(*this); registerMemoryFactory(*this); } diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index d2c193888c..e7767cd4ac 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -1,12 +1,22 @@ +#if XRPL_ROCKSDB_AVAILABLE #include +#include #include #include +#include +#include +#include #include #include #include +#include +#include #include #include #include +#include +#include +#include #include #include @@ -24,26 +34,14 @@ #include #include +#include #include #include #include +#include #include #include -#if XRPL_ROCKSDB_AVAILABLE -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - namespace xrpl::NodeStore { class RocksDBEnv : public rocksdb::EnvWrapper diff --git a/src/test/nodestore/import_test.cpp b/src/test/nodestore/import_test.cpp index a80b5ccc93..d8c4a96713 100644 --- a/src/test/nodestore/import_test.cpp +++ b/src/test/nodestore/import_test.cpp @@ -21,11 +21,16 @@ #include #include #include + +#if XRPL_ROCKSDB_AVAILABLE + #include #include #include #include +#endif + #include #include #include @@ -297,17 +302,17 @@ public: auto const args = parseArgs(arg()); bool usage = args.empty(); - if (!usage && args.find("from") == args.end()) + if (!usage && !args.contains("from")) { log << "Missing parameter: from"; usage = true; } - if (!usage && args.find("to") == args.end()) + if (!usage && !args.contains("to")) { log << "Missing parameter: to"; usage = true; } - if (!usage && args.find("buffer") == args.end()) + if (!usage && !args.contains("buffer")) { log << "Missing parameter: buffer"; usage = true; From 8e3eabc398f1400dec1a9c4c63d9b2dabc0ad78d Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Wed, 3 Jun 2026 11:59:18 -0700 Subject: [PATCH 013/121] refactor: Remove auto-update script and update RPM version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Update RPM version scheme; remove auto-update script; service hardening - **RPM version scheme**: pre-releases now use `~` in the `Version` field instead of the `0..` `Release`-field hack. Matches Debian's `~` convention, so RPM and DEB version strings are symmetric. Requires rpm ≥ 4.10 (RHEL 9 ships 4.17). Before/after for a pre-release build: ``` # before xrpld-3.2.0-0.1.rc3+202606011647.d4cb68d5.el9.x86_64.rpm # after (symmetric with DEB) xrpld-3.2.0~rc2+202606010139.7679a310-1.el9.x86_64.rpm xrpld_3.2.0~rc2+202606010139.7679a310-1_amd64.deb ``` - **Auto-update removed**: `update-xrpld`, `update-xrpld.service`, and `update-xrpld.timer` deleted. The `50-xrpld.preset` `disable` line for the timer is dropped too. - **Service hardening** (two new `[Service]` directives in `xrpld.service`): - `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` — drops every Linux capability except `CAP_NET_BIND_SERVICE`, capping the privilege ceiling to least-privilege while still letting operators bind ports <1024 (e.g. WS/HTTPS on 443). - `SystemCallArchitectures=native` — restricts the service to the native syscall ABI, blocking alternate-ABI (32-bit/x32) syscalls used to evade seccomp filtering. - [ ] Build RPM from a pre-release version (e.g. `3.2.0-b1`) and confirm `rpm -qi` shows `Version: 3.2.0~b1`, `Release: 1` - [ ] Confirm `3.2.0~b1` sorts before `3.2.0` via `rpmvercmp` - [ ] Install package and confirm no `update-xrpld*` units appear in `systemctl list-unit-files` - [ ] Confirm `systemctl show xrpld` reflects the new `CapabilityBoundingSet` and `SystemCallArchitectures` * fix: Track tmpfiles-created directories in RPM %files as %ghost --- package/README.md | 1 - package/build_pkg.sh | 33 +++--- package/debian/rules | 2 - package/debian/xrpld.docs | 1 - package/rpm/xrpld.spec | 19 +--- package/shared/50-xrpld.preset | 2 - package/shared/update-xrpld | 152 ---------------------------- package/shared/update-xrpld.service | 16 --- package/shared/update-xrpld.timer | 10 -- package/shared/xrpld.service | 2 + 10 files changed, 19 insertions(+), 219 deletions(-) delete mode 100755 package/shared/update-xrpld delete mode 100644 package/shared/update-xrpld.service delete mode 100644 package/shared/update-xrpld.timer diff --git a/package/README.md b/package/README.md index 867ca273b4..d440f70fb8 100644 --- a/package/README.md +++ b/package/README.md @@ -15,7 +15,6 @@ package/ xrpld.sysusers sysusers.d config (used by both RPM and DEB) xrpld.tmpfiles tmpfiles.d config (used by both RPM and DEB) xrpld.logrotate logrotate config (installed to /etc/logrotate.d/xrpld) - update-xrpld auto-update script (installed to /usr/libexec/xrpld/, run by update-xrpld.timer) ``` ## Prerequisites diff --git a/package/build_pkg.sh b/package/build_pkg.sh index f2c2c63c12..e2ec8fee3d 100755 --- a/package/build_pkg.sh +++ b/package/build_pkg.sh @@ -114,10 +114,11 @@ VER_BASE="${VERSION%%-*}" VER_SUFFIX="${VERSION#*-}" [[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX="" -# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). The RPM -# Release field forbids '-', and the convention here is single-token suffixes -# like b1 or rc2. Fail early with a clear message rather than letting either -# rpmbuild blow up or silently mangling dashes into dots. +# 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 @@ -142,9 +143,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}/update-xrpld" "${dest}/update-xrpld" - cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service" - cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer" cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset" } @@ -156,20 +154,18 @@ build_rpm() { cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec" stage_common "${topdir}/SOURCES" - # RPM Version can't contain '-'. A pre-release goes in Release with a - # leading "0." so 3.2.0-b1 sorts before the final 3.2.0-. - # The order is "0.." (e.g. 0.1.b6) — the Fedora/EPEL - # convention. Reversing to "0.." (e.g. 0.b6.1) breaks - # rpmvercmp against the former because numeric segments outrank alphabetic - # ones, so "0.1.b5" would sort newer than "0.b6.1". - local rpm_release="${PKG_RELEASE}" - [[ -n "${VER_SUFFIX}" ]] && rpm_release="0.${PKG_RELEASE}.${VER_SUFFIX}" + # 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 ${VER_BASE}" \ - --define "xrpld_release ${rpm_release}" \ + --define "xrpld_version ${rpm_version}" \ + --define "xrpld_release ${PKG_RELEASE}" \ "${topdir}/SPECS/xrpld.spec" } @@ -181,13 +177,10 @@ build_deb() { stage_common "${staging}" cp -r "${DEBIAN_DIR}" "${staging}/debian" - # Debhelper auto-discovers these only from debian/. cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service" cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers" cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles" cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate" - cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service" - cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer" # 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}" diff --git a/package/debian/rules b/package/debian/rules index cd94da7e5b..16574bca3f 100644 --- a/package/debian/rules +++ b/package/debian/rules @@ -10,7 +10,6 @@ override_dh_auto_configure override_dh_auto_build override_dh_auto_test: override_dh_installsystemd: dh_installsystemd --no-stop-on-upgrade xrpld.service - dh_installsystemd --name=update-xrpld --no-start update-xrpld.service update-xrpld.timer execute_before_dh_installtmpfiles: dh_installsysusers @@ -21,7 +20,6 @@ override_dh_install: install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt - install -D -m 0755 update-xrpld debian/xrpld/usr/libexec/xrpld/update-xrpld override_dh_dwz: @: diff --git a/package/debian/xrpld.docs b/package/debian/xrpld.docs index 1217b6db43..b43bf86b50 100644 --- a/package/debian/xrpld.docs +++ b/package/debian/xrpld.docs @@ -1,2 +1 @@ README.md -LICENSE.md diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec index 4933c724f7..5595fd0d8d 100644 --- a/package/rpm/xrpld.spec +++ b/package/rpm/xrpld.spec @@ -35,8 +35,6 @@ install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{ # systemd units, sysusers, tmpfiles, preset install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service -install -Dm0644 %{_sourcedir}/update-xrpld.service %{buildroot}%{_unitdir}/update-xrpld.service -install -Dm0644 %{_sourcedir}/update-xrpld.timer %{buildroot}%{_unitdir}/update-xrpld.timer 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 @@ -44,9 +42,6 @@ install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50- # Logrotate config install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name} -# Update helper -install -Dm0755 %{_sourcedir}/update-xrpld %{buildroot}%{_libexecdir}/%{name}/update-xrpld - # Docs install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md install -Dm0644 %{_sourcedir}/README.md %{buildroot}%{_docdir}/%{name}/README.md @@ -61,10 +56,10 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled %post systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : -%systemd_post xrpld.service update-xrpld.timer +%systemd_post xrpld.service %preun -%systemd_preun xrpld.service update-xrpld.timer +%systemd_preun xrpld.service %postun %systemd_postun_with_restart xrpld.service @@ -74,7 +69,6 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %doc %{_docdir}/%{name}/README.md %dir %{_sysconfdir}/%{name} -%dir %{_libexecdir}/%{name} %{_bindir}/%{name} @@ -82,18 +76,13 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %config(noreplace) %{_sysconfdir}/%{name}/validators.txt %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} -%{_libexecdir}/%{name}/update-xrpld %{_unitdir}/xrpld.service -%{_unitdir}/update-xrpld.service -%{_unitdir}/update-xrpld.timer %{_presetdir}/50-xrpld.preset %{_sysusersdir}/xrpld.conf %{_tmpfilesdir}/xrpld.conf - -%ghost %dir /var/lib/%{name} -%ghost %dir /var/log/%{name} - +%ghost %dir /var/lib/xrpld +%ghost %dir /var/log/xrpld # Legacy compatibility for pre-FHS package layouts. # TODO: remove after rippled fully deprecated. diff --git a/package/shared/50-xrpld.preset b/package/shared/50-xrpld.preset index 6264e00131..bfbcd56577 100644 --- a/package/shared/50-xrpld.preset +++ b/package/shared/50-xrpld.preset @@ -1,4 +1,2 @@ # /usr/lib/systemd/system-preset/50-xrpld.preset enable xrpld.service -# Don't enable automatic updates -disable update-xrpld.timer diff --git a/package/shared/update-xrpld b/package/shared/update-xrpld deleted file mode 100755 index 4bd4db2538..0000000000 --- a/package/shared/update-xrpld +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Optional: also write logs to a legacy file in addition to journald. -# By default, this script logs to systemd/journald, viewable via: -# journalctl -t update-xrpld -# -# Uncomment the line below if you need a flat file for compatibility with -# external tooling, manual inspection, or environments where journald logs -# are not persisted or easily accessible. -# -# Note: This duplicates all output (stdout/stderr) to both journald and the file. -# It is generally not needed on modern systems and may cause log file growth -# if left enabled long-term. -# -# Requires /var/log/xrpld/ to exist and be writable by the service (root). -# -# exec > >(tee -a /var/log/xrpld/update.log) 2>&1 - -PATH=/usr/sbin:/usr/bin:/sbin:/bin - -PKG_NAME=${PKG_NAME:-xrpld} - -log() { - # If running under systemd/journald, let it handle timestamps. - if [[ -n "${JOURNAL_STREAM:-}" ]]; then - printf '%s\n' "$*" - else - printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" - fi -} - -require_root() { - if [[ ${EUID:-$(id -u)} -ne 0 ]]; then - log "RESULT: failed reason=not-root" - exit 1 - fi -} - -get_installed_version() { - if command -v dpkg-query >/dev/null 2>&1; then - dpkg-query -W -f='${Version}' "$PKG_NAME" 2>/dev/null || printf 'unknown' - elif command -v rpm >/dev/null 2>&1; then - rpm -q --qf '%{VERSION}-%{RELEASE}' "$PKG_NAME" 2>/dev/null || printf 'unknown' - else - printf 'unknown' - fi -} - -trap 'log "RESULT: failed reason=script-error exit_code=$?"' ERR - -apt_can_update() { - apt-get update -qq - apt-get -s --only-upgrade install "$PKG_NAME" 2>/dev/null | grep -q "^Inst ${PKG_NAME}\b" -} - -apt_apply_update() { - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ - -o Dpkg::Options::="--force-confdef" \ - -o Dpkg::Options::="--force-confold" \ - "$PKG_NAME" -} - -get_rpm_pm() { - if command -v dnf >/dev/null 2>&1; then - printf 'dnf\n' - elif command -v yum >/dev/null 2>&1; then - printf 'yum\n' - else - return 1 - fi -} - -rpm_refresh_metadata() { - local pm=$1 - if [[ "$pm" == "dnf" ]]; then - dnf makecache --refresh -q >/dev/null - else - yum clean expire-cache -q >/dev/null - fi -} - -rpm_can_update() { - local pm=$1 - - rpm_refresh_metadata "$pm" - local rc=0 - set +e - "$pm" check-update -q "$PKG_NAME" >/dev/null 2>&1 - rc=$? - set -e - - if [[ $rc -eq 100 ]]; then - return 0 - elif [[ $rc -eq 0 ]]; then - return 1 - else - log "$pm check-update failed with exit code ${rc}." - exit 1 - fi -} - -rpm_apply_update() { - local pm=$1 - "$pm" update -y "$PKG_NAME" -} - -restart_service() { - # Preserve the operator's prior service state: if xrpld was intentionally - # stopped before the update, don't bring it back up just because the - # auto-update timer fired. - if systemctl is-active --quiet "${PKG_NAME}.service"; then - systemctl restart "${PKG_NAME}.service" - log "${PKG_NAME} service restarted successfully." - else - log "${PKG_NAME} service was not running; skipping restart to preserve prior state." - fi -} - -main() { - require_root - if command -v apt-get >/dev/null 2>&1; then - log "Checking for ${PKG_NAME} updates via apt" - if apt_can_update; then - log "Update available; installing." - apt_apply_update - restart_service - log "RESULT: updated ${PKG_NAME}=$(get_installed_version)" - else - log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)" - fi - return - fi - - local rpm_pm="" - if rpm_pm="$(get_rpm_pm)"; then - log "Checking for ${PKG_NAME} updates via ${rpm_pm}" - if rpm_can_update "$rpm_pm"; then - log "Update available; installing" - rpm_apply_update "$rpm_pm" - restart_service - log "RESULT: updated ${PKG_NAME}=$(get_installed_version)" - else - log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)" - fi - return - fi - log "RESULT: failed reason=no-package-manager" - exit 1 -} - -main "$@" diff --git a/package/shared/update-xrpld.service b/package/shared/update-xrpld.service deleted file mode 100644 index a964ca5482..0000000000 --- a/package/shared/update-xrpld.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Check for and install xrpld package updates -Documentation=man:systemd.service(5) -Wants=network-online.target -After=network-online.target -ConditionPathExists=/usr/libexec/xrpld/update-xrpld -ConditionPathExists=/usr/bin/xrpld - -[Service] -Type=oneshot -ExecStart=/usr/bin/flock -n /run/lock/xrpld-update.lock /usr/libexec/xrpld/update-xrpld -StandardOutput=journal -StandardError=journal -SyslogIdentifier=update-xrpld -TimeoutStartSec=30min -PrivateTmp=true diff --git a/package/shared/update-xrpld.timer b/package/shared/update-xrpld.timer deleted file mode 100644 index 21dabf1400..0000000000 --- a/package/shared/update-xrpld.timer +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Daily xrpld update check - -[Timer] -OnCalendar=*-*-* 00:00:00 -RandomizedDelaySec=24h -Persistent=true - -[Install] -WantedBy=timers.target diff --git a/package/shared/xrpld.service b/package/shared/xrpld.service index 72b6cc9938..0dd4e3a791 100644 --- a/package/shared/xrpld.service +++ b/package/shared/xrpld.service @@ -17,6 +17,8 @@ PrivateTmp=true User=xrpld Group=xrpld LimitNOFILE=65536 +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +SystemCallArchitectures=native [Install] WantedBy=multi-user.target From e833e8884d01bdf5f1088215a4709c5b3d3b4aae Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:29:09 +0100 Subject: [PATCH 014/121] refactor: Revert "Explicitly trim the heap after cache sweeps (#6022)" --- src/xrpld/app/main/Application.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 508dfc8590..15522b9806 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -43,7 +43,6 @@ #include #include #include -#include #include #include #include @@ -1088,8 +1087,6 @@ public: << "; size after: " << cachedSLEs_.size(); } - mallocTrim("doSweep", journal_); - // Set timer to do another sweep later. setSweepTimer(); } From fded06652ad3d85977e36af903548425e8ff8094 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Wed, 3 Jun 2026 15:57:34 -0400 Subject: [PATCH 015/121] fix: Add zero NFT Offer ID check for NFTokenCancelOffer --- .../tx/transactors/nft/NFTokenCancelOffer.cpp | 11 +++++++++-- src/test/app/NFToken_test.cpp | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp index 924dc49269..e00cd53685 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -23,8 +24,14 @@ namespace xrpl { NotTEC NFTokenCancelOffer::preflight(PreflightContext const& ctx) { - if (auto const& ids = ctx.tx[sfNFTokenOffers]; - ids.empty() || (ids.size() > kMaxTokenOfferCancelCount)) + auto const& offerIds = ctx.tx[sfNFTokenOffers]; + + if (offerIds.empty() || (offerIds.size() > kMaxTokenOfferCancelCount)) + return temMALFORMED; + + // Zero offer IDs cannot be passed as ledger entry keys. + if (ctx.rules.enabled(fixCleanup3_2_0) && + std::ranges::any_of(offerIds, [](uint256 const& id) { return id.isZero(); })) return temMALFORMED; // In order to prevent unnecessarily overlarge transactions, we diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ebd470ec92..269bc72c53 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -892,6 +892,25 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, buyer) == 1); } + // Only test this with fixCleanup3_2_0 enabled. Without the fix, + // an assert-enabled build can crash when Ledger::read() receives + // a zero-key offer ID. + if (features[fixCleanup3_2_0]) + { + // Zero is not a valid offer ID. + env(token::cancelOffer(buyer, {uint256{}}), Ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + + // 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; + env(token::cancelOffer(buyer, {buyerOfferIndex, uint256{}, craftedIndex}), + Ter(temMALFORMED)); + env.close(); + BEAST_EXPECT(ownerCount(env, buyer) == 1); + } + // List of tokens to delete is too long. { std::vector const offers(kMaxTokenOfferCancelCount + 1, buyerOfferIndex); From 61dae6f79249a829b9204ba323250324c36ea273 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 3 Jun 2026 16:18:08 -0400 Subject: [PATCH 016/121] release: Bump version to 3.2.0-rc5 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index e81be00920..ab83236bb5 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0-rc4" +char const* const versionString = "3.2.0-rc5" // clang-format on ; From 96d0563ea644ba5bb28e08e37dc7752d05a204fb Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Thu, 4 Jun 2026 16:23:33 -0700 Subject: [PATCH 017/121] fix: Adjust xrpld systemd service --- package/shared/xrpld.service | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/package/shared/xrpld.service b/package/shared/xrpld.service index 0dd4e3a791..7f3496acbb 100644 --- a/package/shared/xrpld.service +++ b/package/shared/xrpld.service @@ -2,14 +2,15 @@ Description=XRP Ledger Daemon After=network-online.target Wants=network-online.target -StartLimitIntervalSec=300 +StartLimitIntervalSec=5min StartLimitBurst=5 [Service] Type=simple ExecStart=/usr/bin/xrpld --net --silent --conf /etc/xrpld/xrpld.cfg -Restart=always +Restart=on-failure RestartSec=5s +TimeoutStopSec=5min NoNewPrivileges=true ProtectSystem=full ProtectHome=true @@ -17,8 +18,11 @@ PrivateTmp=true User=xrpld Group=xrpld LimitNOFILE=65536 -CapabilityBoundingSet=CAP_NET_BIND_SERVICE SystemCallArchitectures=native +# Uncomment both lines to allow xrpld to bind to privileged ports (<1024) +#CapabilityBoundingSet=CAP_NET_BIND_SERVICE +#AmbientCapabilities=CAP_NET_BIND_SERVICE + [Install] WantedBy=multi-user.target From e5785c4fcbf45eb9fb80c87a2b19831badd791c1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Jun 2026 15:57:23 -0400 Subject: [PATCH 018/121] fix: Fix Number comparison operator --- include/xrpl/basics/Number.h | 35 ++++++----- src/test/basics/Number_test.cpp | 104 ++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 93bef82a8c..cee0c45355 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -408,33 +408,40 @@ public: } friend constexpr bool - operator<(Number const& x, Number const& y) noexcept + operator<(Number const& l, Number const& r) noexcept { + bool const lneg = l.negative_; + bool const rneg = r.negative_; + // If the two amounts have different signs (zero is treated as positive) // then the comparison is true iff the left is negative. - bool const lneg = x.negative_; - bool const rneg = y.negative_; - if (lneg != rneg) return lneg; - // Both have same sign and the left is zero: the right must be - // greater than 0. - if (x.mantissa_ == 0) - return y.mantissa_ > 0; + // Both have same sign and the left is zero: both must be non-negative. + // If the right is greater than 0, then it is larger, so the comparison is true. + if (l.mantissa_ == 0) + return r.mantissa_ > 0; - // Both have same sign, the right is zero and the left is non-zero. - if (y.mantissa_ == 0) + // Both have same sign, the right is zero and the left is non-zero, so the left must be + // positive, and thus is larger, so the comparison is false. + if (r.mantissa_ == 0) return false; // Both have the same sign, compare by exponents: - if (x.exponent_ > y.exponent_) + if (l.exponent_ > r.exponent_) return lneg; - if (x.exponent_ < y.exponent_) + if (l.exponent_ < r.exponent_) return !lneg; - // If equal exponents, compare mantissas - return x.mantissa_ < y.mantissa_; + // If equal signs and exponents, compare mantissas. + if (lneg) + { + // If negative, the operator is reversed. + return l.mantissa_ > r.mantissa_; + } + + return l.mantissa_ < r.mantissa_; } /** Return the sign of the amount */ diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 81019970ad..f4bd1c9d66 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,8 @@ #include #include #include +#include +#include namespace xrpl { @@ -1386,10 +1389,103 @@ public: testRelationals() { testcase << "test_relationals " << to_string(Number::getMantissaScale()); - BEAST_EXPECT(!(Number{100} < Number{10})); - BEAST_EXPECT(Number{100} > Number{10}); - BEAST_EXPECT(Number{100} >= Number{10}); - BEAST_EXPECT(!(Number{100} <= Number{10})); + + { + auto test = [this](auto const& nums) { + BEAST_EXPECT(std::ranges::is_sorted(nums)); + + for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1) + { + auto iter2 = iter1; + for (++iter2; iter2 != nums.end(); ++iter2) + { + Number const& smaller = *iter1; + Number const& larger = *iter2; + std::stringstream ss; + ss << smaller << " < " << larger; + auto const str = ss.str(); + + // The ==/!= operators use a completely different code path than <, etc. + // This helps detect a breakage in one but not the other. It also helps + // verify that the values are being ordered correctly. + BEAST_EXPECTS(smaller != larger, str + " (!=)"); + BEAST_EXPECTS(!(smaller == larger), str + " (==)"); + + // true results using operator< and derived operators + BEAST_EXPECTS(smaller < larger, str + " (<)"); + BEAST_EXPECTS(larger > smaller, str + " (>)"); + BEAST_EXPECTS(larger >= smaller, str + " (>=)"); + BEAST_EXPECTS(smaller <= larger, str + " (<=)"); + + // false results using operator< and derived operators + BEAST_EXPECTS(!(larger < smaller), str + " (! <)"); + BEAST_EXPECTS(!(smaller > larger), str + " (! >)"); + BEAST_EXPECTS(!(smaller >= larger), str + " (! >=)"); + BEAST_EXPECTS(!(larger <= smaller), str + " (! <=)"); + } + } + }; + + auto const intNums = [this]() { + // Inequality test cases are built from a list of sorted integers + auto const values = + std::to_array({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100}); + // Check this list is sorted before converting it to Numbers. + // That way if any of the other tests fail, we know it's because of code and not the + // source data. + BEAST_EXPECT(std::ranges::is_sorted(values)); + + std::vector result; + result.reserve(values.size()); + for (auto const v : values) + result.emplace_back(v); + return result; + }(); + + auto const otherNums = std::to_array({ + Number{-5, 100}, + Number{-1, 100}, + Number{-7, -10}, + Number{-2, -10}, + Number{0}, + Number{2, -10}, + Number{7, -10}, + Number{1, 100}, + Number{5, 100}, + }); + + test(intNums); + test(otherNums); + } + + { + // Equality test cases are . Number will be compared against itself + using Case = std::pair; + auto const c = std::to_array({ + {700, __LINE__}, + {50, __LINE__}, + {1, __LINE__}, + {0, __LINE__}, + {-1, __LINE__}, + {-30, __LINE__}, + {-600, __LINE__}, + }); + for (auto const& [n, line] : c) + { + auto const str = to_string(n); + + // NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with + // equivalent values + expect(n == n, str + " ==", __FILE__, line); + expect(!(n != n), str + " !=", __FILE__, line); + + expect(!(n < n), str + " < ", __FILE__, line); + expect(!(n > n), str + " >", __FILE__, line); + expect(n >= n, str + " >=", __FILE__, line); + expect(n <= n, str + " <=", __FILE__, line); + // NOLINTEND(misc-redundant-expression) + } + } } void From 781ef175c9e0826f12da0e8d9557eeb68c5c516a Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:23:41 +0200 Subject: [PATCH 019/121] perf: Dispatch "hasInvalidAmount()" on type tag instead of dynamic_cast --- src/libxrpl/protocol/STAmount.cpp | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 1ba9cd042f..ddb2be29cf 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1250,16 +1250,34 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j) return true; } - if (auto const amount = dynamic_cast(&field)) - return !isLegalMPT(*amount) || !isLegalNet(*amount); + // Dispatch on the serialized type tag rather than RTTI: this is on the invariant-checking path + // and a dynamic_cast chain over every field of every modified entry is measurably expensive. + // The object-like tags below all denote STObject subclasses (STLedgerEntry, STTx), so the + // downcast is sound; nested fields are only ever plain STI_OBJECT / STI_ARRAY containers. + // safeDowncast keeps a dynamic_cast validity assert in debug builds while compiling to + // static_cast in release. + switch (field.getSType()) + { + case STI_AMOUNT: { + auto const& amount = safeDowncast(field); + return !isLegalMPT(amount) || !isLegalNet(amount); + } - if (auto const object = dynamic_cast(&field)) - return hasInvalidAmount(*object, depth + 1, j); + case STI_OBJECT: + case STI_LEDGERENTRY: + case STI_TRANSACTION: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - if (auto const array = dynamic_cast(&field)) - return hasInvalidAmount(*array, depth + 1, j); + case STI_ARRAY: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - return false; + default: { + XRPL_ASSERT( + dynamic_cast(&field) == nullptr, + "xrpl::hasInvalidAmount : valid object type"); + return false; + } + } } bool From ed5f13481a444380e7821d98d2ce73d316a77744 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:49:49 +0200 Subject: [PATCH 020/121] fix: Disable transaction invariants --- src/libxrpl/tx/Transactor.cpp | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 28fa059902..1aa7567c6c 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1173,21 +1173,17 @@ Transactor::checkTransactionInvariants(TER result, XRPAmount fee) [[nodiscard]] TER Transactor::checkInvariants(TER result, XRPAmount fee) { - // Transaction invariants first (more specific). These check post-conditions of the specific - // transaction. If these fail, the transaction's core logic is wrong. - auto const txResult = checkTransactionInvariants(result, fee); - - // Protocol invariants second (broader). These check properties that must hold regardless of - // transaction type. - auto const protoResult = ctx_.checkInvariants(result, fee); - - // Fail if either check failed. tef (fatal) takes priority over tec. - if (protoResult == tefINVARIANT_FAILED) - return tefINVARIANT_FAILED; - if (txResult == tecINVARIANT_FAILED || protoResult == tecINVARIANT_FAILED) - return tecINVARIANT_FAILED; - - return result; + /* + * DISABLED for 3.2.0 — Must be re-introduced for 3.3.0 + * + * Transaction invariants are disabled due to a performance regression: + * the two-pass design (transaction-specific invariants + protocol invariants) + * iterates over modified ledger entries twice per transaction. + * + * Until resolved, only protocol invariants are checked (delegated to ctx_). + * This is safe because all transaction invariants in 3.2.0 are no-ops. + */ + return ctx_.checkInvariants(result, fee); } //------------------------------------------------------------------------------ ApplyResult From 0ac8e6cf1ebf14d9ff1e6df700ce603fae507163 Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 5 Jun 2026 17:30:12 -0400 Subject: [PATCH 021/121] release: Bump version to 3.2.0-rc6 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index ab83236bb5..b704f50813 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0-rc5" +char const* const versionString = "3.2.0-rc6" // clang-format on ; From 6b63f0ff614e090c8a782d63591e4b035c7715ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:37:50 -0400 Subject: [PATCH 022/121] ci: [DEPENDABOT] bump codecov/codecov-action from 6.0.1 to 7.0.0 (#7426) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/reusable-build-test-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 31457bb892..4163e17779 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -324,7 +324,7 @@ jobs: - name: Upload coverage report if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }} - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: disable_search: true disable_telem: true From 3c43f4614f87965298773279ff5b85d4c56c637b Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 15 Jun 2026 22:19:38 +0100 Subject: [PATCH 023/121] release: Bump version to 3.2.0 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index b704f50813..c488bb20de 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0-rc6" +char const* const versionString = "3.2.0" // clang-format on ; From 0364e4dc4197cf8be2ac84ea8acdc438b72ad17e Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 16 Jun 2026 14:24:12 +0100 Subject: [PATCH 024/121] docs: Rewrite build environment docs (#7533) Co-authored-by: Ed Hennis --- .github/scripts/strategy-matrix/linux.json | 6 +- .github/workflows/build-nix-images.yml | 8 + .github/workflows/publish-docs.yml | 2 +- .../workflows/reusable-build-test-config.yml | 5 + .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/reusable-upload-recipe.yml | 2 +- BUILD.md | 391 ++++-------------- CONTRIBUTING.md | 16 +- bin/check-tools.sh | 158 +++++++ {nix/docker => bin}/install-sanitizer-libs.sh | 0 cspell.config.yaml | 1 + docs/build/advanced_conan.md | 193 +++++++++ docs/build/conan.md | 2 +- docs/build/environment.md | 162 +++----- docs/build/nix.md | 45 +- docs/build/nix_troubleshooting.md | 61 +++ nix/docker/Dockerfile | 4 +- nix/docker/README.md | 90 ++++ nix/docker/check-tools.sh | 39 -- 19 files changed, 710 insertions(+), 477 deletions(-) create mode 100755 bin/check-tools.sh rename {nix/docker => bin}/install-sanitizer-libs.sh (100%) create mode 100644 docs/build/advanced_conan.md create mode 100644 docs/build/nix_troubleshooting.md create mode 100644 nix/docker/README.md delete mode 100755 nix/docker/check-tools.sh diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 4f45216cda..a9b85b766a 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -1,5 +1,5 @@ { - "image_tag": "sha-63ffdc3", + "image_tag": "sha-fe4c8ae", "configs": { "ubuntu": [ { @@ -68,7 +68,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], - "image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-63ffdc3" + "image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745" } ], @@ -77,7 +77,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], - "image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-63ffdc3" + "image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745" } ] } diff --git a/.github/workflows/build-nix-images.yml b/.github/workflows/build-nix-images.yml index 24f069902d..3af6a3b1d4 100644 --- a/.github/workflows/build-nix-images.yml +++ b/.github/workflows/build-nix-images.yml @@ -9,12 +9,20 @@ on: - "flake.nix" - "flake.lock" - "nix/**" + - "!nix/docker/README.md" + - "!nix/devshell.nix" + - "bin/check-tools.sh" + - "bin/install-sanitizer-libs.sh" pull_request: paths: - ".github/workflows/build-nix-images.yml" - "flake.nix" - "flake.lock" - "nix/**" + - "!nix/docker/README.md" + - "!nix/devshell.nix" + - "bin/check-tools.sh" + - "bin/install-sanitizer-libs.sh" workflow_dispatch: concurrency: diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index bcf5968384..0de5347aab 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-63ffdc3 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 28d317e4dd..95e6b0cbe2 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -121,6 +121,11 @@ jobs: if: ${{ inputs.ccache_enabled && runner.debug == '1' }} run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}" + - name: Check tools + env: + CHECK_TOOLS_SKIP_CLONE: "1" + run: ./bin/check-tools.sh + - name: Print build environment uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574 diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 9f10711b6f..34fa860e12 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -36,7 +36,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-63ffdc3" + container: "ghcr.io/xrplf/xrpld/nix-debian:sha-fe4c8ae" permissions: contents: read issues: write diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index 1c90fb0e72..ba7a0943d9 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-63ffdc3 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/BUILD.md b/BUILD.md index 662ba0d33d..2ac24f2c5d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,26 +1,57 @@ -| :warning: **WARNING** :warning: | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md). | +| :warning: **WARNING** :warning: | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).

These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. | -> These instructions also assume a basic familiarity with Conan and CMake. -> If you are unfamiliar with Conan, you can read our -> [crash course](./docs/build/conan.md) or the official [Getting Started][3] -> walkthrough. +## Minimum Requirements -## Branches +See [System Requirements](https://xrpl.org/system-requirements.html). -For a stable release, choose the `master` branch or one of the [tagged -releases](https://github.com/XRPLF/rippled/releases). +Building xrpld generally requires Git, Python, Conan, CMake, and a C++ +compiler. + +- [Python](https://www.python.org/downloads/) +- [Conan](https://conan.io/downloads.html) +- [CMake](https://cmake.org/download/) + +You can verify that the required tools are installed and runnable with: ```bash -git checkout master +./bin/check-tools.sh ``` -For the latest release candidate, choose the `release` branch. +`xrpld` is written in the C++23 dialect. The [tested compiler versions][cpp23-support] are: -```bash -git checkout release -``` +| Compiler | Version | +| ----------- | --------------- | +| GCC | 15.2 | +| Clang | 22 | +| Apple Clang | 17 | +| MSVC | 19.44[^windows] | + +## Operating Systems + +Please see the [environment setup guide](./docs/build/environment.md) for detailed instructions for all platforms. + +### Linux + +The Ubuntu Linux distribution has received the highest level of quality +assurance, testing, and support. We also support Red Hat and use Debian +internally. +Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them. + +### macOS + +Many `xrpld` engineers use macOS for development. + +### Windows + +Windows is used by some engineers for development only. + +[^windows]: Windows is not recommended for production use. + +## Steps + +### Branches For the latest set of untested features, or to contribute, choose the `develop` branch. @@ -29,55 +60,15 @@ branch. git checkout develop ``` -## Minimum Requirements +For a release candidate, choose the relevant release branch, e.g. +`release/3.2.x`. -See [System Requirements](https://xrpl.org/system-requirements.html). +```bash +git checkout release/3.2.x +``` -Building xrpld generally requires git, Python, Conan, CMake, and a C++ -compiler. Some guidance on setting up such a [C++ development environment can be -found here](./docs/build/environment.md). - -- [Python 3.11](https://www.python.org/downloads/), or higher -- [Conan 2.17](https://conan.io/downloads.html)[^1], or higher -- [CMake 3.22](https://cmake.org/download/), or higher - -[^1]: - It is possible to build with Conan 1.60+, but the instructions are - significantly different, which is why we are not recommending it. - -`xrpld` is written in the C++23 dialect and includes the `` header. -The [tested compiler versions][2] are: - -| Compiler | Version | -| ----------- | --------- | -| GCC | 15 | -| Clang | 22 | -| Apple Clang | 17 | -| MSVC | 19.44[^3] | - -### Linux - -The Ubuntu Linux distribution has received the highest level of quality -assurance, testing, and support. We also support Red Hat and use Debian -internally. - -Here are [sample instructions for setting up a C++ development environment on -Linux](./docs/build/environment.md#linux). - -### Mac - -Many xrpld engineers use macOS for development. - -Here are [sample instructions for setting up a C++ development environment on -macOS](./docs/build/environment.md#macos). - -### Windows - -Windows is used by some engineers for development only. - -[^3]: Windows is not recommended for production use. - -## Steps +For a stable release, choose one of the [tagged +releases](https://github.com/XRPLF/rippled/releases). ### Set Up Conan @@ -86,18 +77,11 @@ Conan, CMake, and a C++ compiler, you may need to set up your Conan profile. These instructions assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, then please read [this crash course](./docs/build/conan.md) or the official -[Getting Started][3] walkthrough. +[Getting Started][conan-getting-started] walkthrough. -#### Conan lockfile +#### Profiles -To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html), -which has to be updated every time dependencies change. - -Please see the [instructions on how to regenerate the lockfile](conan/lockfile/README.md). - -#### Default profile - -We recommend that you import the provided `conan/profiles/default` profile: +We recommend that you install our Conan profiles: ```bash conan config install conan/profiles/ -tf $(conan config home)/profiles/ @@ -109,222 +93,15 @@ You can check your Conan profile by running: conan profile show ``` -#### Custom profile +If the default profile is not suitable for your environment, you can create a custom profile and pass it to Conan. +More information on customizing Conan can be found in the [Advanced Conan configuration](./docs/build/advanced_conan.md). -If the default profile does not work for you and you do not yet have a Conan -profile, you can create one by running: +#### Add xrplf remote + +Run the following command to add the `xrplf` remote, which hosts some of our dependencies: ```bash -conan profile detect -``` - -You may need to make changes to the profile to suit your environment. You can -refer to the provided `conan/profiles/default` profile for inspiration, and you -may also need to apply the required [tweaks](#conan-profile-tweaks) to this -default profile. - -### Patched recipes - -Occasionally, we need patched recipes or recipes not present in Conan Center. -We maintain a fork of the Conan Center Index -[here](https://github.com/XRPLF/conan-center-index/) containing the modified and newly added recipes. - -To ensure our patched recipes are used, you must add our Conan remote at a -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 xrplf https://conan.ripplex.io -``` - -Alternatively, you can pull our recipes from the repository and export them locally: - -```bash -# Define which recipes to export. -recipes=('abseil' 'ed25519' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi') - -# Selectively check out the recipes from our CCI fork. -cd external -mkdir -p conan-center-index -cd conan-center-index -git init -git remote add origin git@github.com:XRPLF/conan-center-index.git -git sparse-checkout init -for recipe in "${recipes[@]}"; do - echo "Checking out recipe '${recipe}'..." - git sparse-checkout add recipes/${recipe} -done -git fetch origin master -git checkout master - -./export_all.sh -cd ../../ -``` - -In the case we switch to a newer version of a dependency that still requires a -patch or add a new dependency, it will be necessary for you to pull in the changes and re-export the -updated dependencies with the newer version. However, if we switch to a newer -version that no longer requires a patch, no action is required on your part, as -the new recipe will be automatically pulled from the official Conan Center. - -> [!NOTE] -> You might need to add `--lockfile=""` to your `conan install` command -> to avoid automatic use of the existing `conan.lock` file when you run -> `conan export` manually on your machine -> -> This is not recommended though, as you might end up using different revisions of recipes. - -### Conan profile tweaks - -#### Missing compiler version - -If you see an error similar to the following after running `conan profile show`: - -```text -ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value. -Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1', -'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15', -'15.0', '16', '16.0'] -Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting" -``` - -you need to add your compiler to the list of compiler versions in -`$(conan config home)/settings_user.yml`, by adding the required version number(s) -to the `version` array specific for your compiler. For example: - -```yaml -compiler: - apple-clang: - version: ["17.0"] -``` - -#### Multiple compilers - -If you have multiple compilers installed, make sure to select the one to use in -your default Conan configuration **before** running `conan profile detect`, by -setting the `CC` and `CXX` environment variables. - -For example, if you are running MacOS and have [homebrew -LLVM@18](https://formulae.brew.sh/formula/llvm@18), and want to use it as a -compiler in the new Conan profile: - -```bash -export CC=$(brew --prefix llvm@18)/bin/clang -export CXX=$(brew --prefix llvm@18)/bin/clang++ -conan profile detect -``` - -You should also explicitly set the path to the compiler in the profile file, -which helps to avoid errors when `CC` and/or `CXX` are set and disagree with the -selected Conan profile. For example: - -```text -[conf] -tools.build:compiler_executables={'c':'/usr/bin/gcc','cpp':'/usr/bin/g++'} -``` - -#### Multiple profiles - -You can manage multiple Conan profiles in the directory -`$(conan config home)/profiles`, for example renaming `default` to a different -name and then creating a new `default` profile for a different compiler. - -#### Select language - -The default profile created by Conan will typically select different C++ dialect -than C++23 used by this project. You should set `23` in the profile line -starting with `compiler.cppstd=`. For example: - -```bash -sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)/profiles/default -``` - -#### Select standard library in Linux - -**Linux** developers will commonly have a default Conan [profile][] that -compiles with GCC and links with libstdc++. If you are linking with libstdc++ -(see profile setting `compiler.libcxx`), then you will need to choose the -`libstdc++11` ABI: - -```bash -sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan config home)/profiles/default -``` - -#### Select architecture and runtime in Windows - -**Windows** developers may need to use the x64 native build tools. An easy way -to do that is to run the shortcut "x64 Native Tools Command Prompt" for the -version of Visual Studio that you have installed. - -Windows developers must also build `xrpld` and its dependencies for the x64 -architecture: - -```bash -sed -i.bak -e 's|^arch=.*$|arch=x86_64|' $(conan config home)/profiles/default -``` - -**Windows** developers also must select static runtime: - -```bash -sed -i.bak -e 's|^compiler\.runtime=.*$|compiler.runtime=static|' $(conan config home)/profiles/default -``` - -#### Clang workaround for grpc - -If your compiler is clang, version 19 or later, or apple-clang, version 17 or -later, you may encounter a compilation error while building the `grpc` -dependency: - -```text -In file included from .../lib/promise/try_seq.h:26: -.../lib/promise/detail/basic_seq.h:499:38: error: a template argument list is expected after a name prefixed by the template keyword [-Wmissing-template-arg-list-after-template-kw] - 499 | Traits::template CallSeqFactory(f_, *cur_, std::move(arg))); - | ^ -``` - -The workaround for this error is to add two lines to profile: - -```text -[conf] -tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw'] -``` - -#### Workaround for gcc 12 - -If your compiler is gcc, version 12, and you have enabled `werr` option, you may -encounter a compilation error such as: - -```text -/usr/include/c++/12/bits/char_traits.h:435:56: error: 'void* __builtin_memcpy(void*, const void*, long unsigned int)' accessing 9223372036854775810 or more bytes at offsets [2, 9223372036854775807] and 1 may overlap up to 9223372036854775813 bytes at offset -3 [-Werror=restrict] - 435 | return static_cast(__builtin_memcpy(__s1, __s2, __n)); - | ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~ -cc1plus: all warnings being treated as errors -``` - -The workaround for this error is to add two lines to your profile: - -```text -[conf] -tools.build:cxxflags=['-Wno-restrict'] -``` - -#### Workaround for clang 16 - -If your compiler is clang, version 16, you may encounter compilation error such -as: - -```text -In file included from .../boost/beast/websocket/stream.hpp:2857: -.../boost/beast/websocket/impl/read.hpp:695:17: error: call to 'async_teardown' is ambiguous - async_teardown(impl.role, impl.stream(), - ^~~~~~~~~~~~~~ -``` - -The workaround for this error is to add two lines to your profile: - -```text -[conf] -tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS'] +conan remote add --index 0 --force xrplf https://conan.ripplex.io ``` ### Set Up Ccache @@ -333,14 +110,7 @@ To speed up repeated compilations, we recommend that you install [ccache](https://ccache.dev), a tool that wraps your compiler so that it can cache build objects locally. -#### Linux - -You can install it using the package manager, e.g. `sudo apt install ccache` -(Ubuntu) or `sudo dnf install ccache` (RHEL). - -#### macOS - -You can install it using Homebrew, i.e. `brew install ccache`. +On Linux and macOS, `ccache` is included in the [Nix development shell](./docs/build/nix.md). #### Windows @@ -549,7 +319,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details. | Option | Default Value | Description | | ---------- | ------------- | -------------------------------------------------------------- | -| `assert` | OFF | Enable assertions. | +| `assert` | OFF | Force enabling assertions. | | `coverage` | OFF | Prepare the coverage report. | | `tests` | OFF | Build tests. | | `unity` | OFF | Configure a unity build. | @@ -557,7 +327,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details. | `werr` | OFF | Treat compilation warnings as errors | | `wextra` | OFF | Enable additional compilation warnings | -[Unity builds][5] may be faster for the first build (at the cost of much more +[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. @@ -583,14 +353,14 @@ After any updates or changes to dependencies, you may need to do the following: conan remove '*' ``` -3. Re-run [conan export](#patched-recipes) if needed. -4. [Regenerate lockfile](#conan-lockfile). +3. Re-run [conan export](./docs/build/advanced_conan.md#patched-recipes) if needed. +4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile). 5. Re-run [conan install](#build-and-test). #### ERROR: Package not resolved If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, -please add `xrplf` remote or re-run `conan export` for [patched recipes](#patched-recipes). +please [add `xrplf` remote](#add-xrplf-remote) or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes). ### `protobuf/port_def.inc` file not found @@ -610,28 +380,9 @@ For example, if you want to build Debug: 1. For conan install, pass `--settings build_type=Debug` 2. For cmake, pass `-DCMAKE_BUILD_TYPE=Debug` -## Add a Dependency - -If you want to experiment with a new package, follow these steps: - -1. Search for the package on [Conan Center](https://conan.io/center/). -2. Modify [`conanfile.py`](./conanfile.py): - - Add a version of the package to the `requires` property. - - Change any default options for the package by adding them to the - `default_options` property (with syntax `'$package:$option': $value`). -3. Modify [`CMakeLists.txt`](./CMakeLists.txt): - - Add a call to `find_package($package REQUIRED)`. - - Link a library from the package to the target `xrpl_libs` - (search for the existing call to `target_link_libraries(xrpl_libs INTERFACE ...)`). -4. Start coding! Don't forget to include whatever headers you need from the package. - -[1]: https://github.com/conan-io/conan-center-index/issues/13168 -[2]: https://en.cppreference.com/w/cpp/compiler_support/20 -[3]: https://docs.conan.io/en/latest/getting_started.html -[5]: https://en.wikipedia.org/wiki/Unity_build -[6]: https://github.com/boostorg/beast/issues/2648 -[7]: https://github.com/boostorg/beast/issues/2661 +[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23 +[conan-getting-started]: https://docs.conan.io/en/latest/getting_started.html +[unity-build]: https://en.wikipedia.org/wiki/Unity_build [gcovr]: https://gcovr.com/en/stable/getting-started.html [python-pip]: https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/ [build_type]: https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html -[profile]: https://docs.conan.io/en/latest/reference/profiles.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25dd7ac059..fc93223925 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,9 @@ The following branches exist in the main project repository: - `develop`: The latest set of unreleased features, and the most common starting point for contributions. -- `release`: The latest beta release or release candidate. -- `master`: The latest stable release. -- `gh-pages`: The documentation for this project, built by Doxygen. +- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line, + holding the latest release candidate, or stable release for that line. + Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases). The tip of each branch must be signed. In order for GitHub to sign a squashed commit that it builds from your pull request, GitHub must know @@ -130,11 +130,9 @@ tl;dr ## Pull requests In general, pull requests use `develop` as the base branch. -The exceptions are -- Fixes and improvements to a release candidate use `release` as the - base. -- Hotfixes use `master` as the base. +The exceptions are fixes, improvements, and hotfixes for an existing release, +which use that release's branch (e.g. `release/3.2.x`) as the base. If your changes are not quite ready, but you want to make it easily available for preliminary examination or review, you can create a "Draft" pull request. @@ -216,7 +214,7 @@ coherent rather than a set of _thou shalt not_ commandments. ## Formatting -All code must conform to `clang-format` version 21, +All code must conform to `clang-format` version 22, according to the settings in [`.clang-format`](./.clang-format), unless the result would be unreasonably difficult to read or maintain. To demarcate lines that should be left as-is, surround them with comments like @@ -261,7 +259,7 @@ This ensures that configuration changes don't introduce new warnings across the ### Installing clang-tidy -See the [environment setup guide](./docs/build/environment.md#clang-tidy) for platform-specific installation instructions. +See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy. ### Running clang-tidy locally diff --git a/bin/check-tools.sh b/bin/check-tools.sh new file mode 100755 index 0000000000..15b16b6fc8 --- /dev/null +++ b/bin/check-tools.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# +# check-tools.sh — verify the xrpld development tooling is present and runnable. +# +# Works on Linux, macOS, and Windows (Git Bash / MSYS). For every expected tool +# it runs a version probe, collecting anything that is missing or fails to run, +# and prints a summary at the end (exiting non-zero if anything is missing). +# +# The tool set is platform-aware: +# - Linux: the full Nix CI environment (see nix/packages.nix, nix/ci-env.nix), +# with GCC, Clang and the sanitizer/coverage tooling. This script is +# run during the Nix Docker image build (nix/docker/Dockerfile), so +# the Linux list is kept in sync with that environment. +# - macOS: the same tooling, minus GCC/g++/gcov/mold +# - Windows: the core build tools only (CMake, Conan, Git, Python). +# MSVC is expected to be provided separately and is not checked here. +# +# Some tools (clang-format, doxygen, gcovr, gh, git-cliff, gpg, pre-commit, +# run-clang-tidy) are present in our Linux CI images and in local development +# setups, but not in the macOS CI environment. They are checked everywhere +# except when running in CI on macOS. +# +# Environment variables: +# CI if set, skip the tools above when on macOS. +# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check. + +set -uo pipefail + +missing=() +checked=0 + +# check [probe-command...] +# Runs the probe (default: " --version") quietly. Records as +# missing if the command is not found or exits non-zero. +check() { + local name="$1" + shift + local -a probe=("$@") + if [ "${#probe[@]}" -eq 0 ]; then + probe=("${name}" --version) + fi + + echo "Checking ${name}..." + checked=$((checked + 1)) + if "${probe[@]}" | head -n 1; then + printf ' [ ok ] %s\n' "${name}" + else + printf ' [MISS] %s\n' "${name}" + missing+=("${name}") + fi +} + +case "$(uname -s)" in + Linux*) os=linux ;; + Darwin*) os=macos ;; + MINGW* | MSYS* | CYGWIN*) os=windows ;; + *) + echo "Unknown OS: $(uname -s)" >&2 + exit 1 + ;; +esac + +echo "Detected OS: ${os} ($(uname -s) $(uname -m))" +echo +echo "Core build tools:" +check cmake +check conan +check git +if [ "${os}" = "windows" ]; then + check python python --version +else + check python3 +fi + +# The full development toolchain. Available from Nix on Linux and macOS; on +# Windows these are typically not installed, so they are skipped. +if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then + echo + echo "Development tooling:" + check ccache + check clang + check clang++ + check ClangBuildAnalyzer + check curl + check file + check less + check make + check netstat which netstat + check ninja + check perl + check pkg-config + check vim + + # 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 doxygen + check gcovr + check gh + check git-cliff + check gpg + # pre-commit, or its alternative implementation prek + check pre-commit sh -c 'pre-commit --version || prek --version' + check run-clang-tidy run-clang-tidy --help + fi +fi + +# GCC is the default compiler on Linux. macOS uses the system Apple Clang +# instead, so GCC/g++/gcov are not expected there. +if [ "${os}" = "linux" ]; then + echo + echo "GCC toolchain:" + check gcc + check g++ + check gcov + + echo + echo "Mold:" + check mold +fi + +if [ "${os}" = "windows" ]; then + echo + echo "Note: on Windows the C++ compiler is MSVC, which is provided" + echo " separately (e.g. via Visual Studio) and is not checked here." +fi + +# A simple test to verify that git can clone a repository over HTTPS +# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up. +if [ -n "${CHECK_TOOLS_SKIP_CLONE:-}" ]; then + echo + echo "Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set)." +else + echo + echo "Connectivity check:" + checked=$((checked + 1)) + tmp_clone="$(mktemp -d)" + if git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions" >/dev/null 2>&1; then + printf ' [ ok ] git clone over HTTPS\n' + else + printf ' [MISS] git clone over HTTPS\n' + missing+=("git-https-clone") + fi + rm -rf "${tmp_clone}" +fi + +echo +if [ "${#missing[@]}" -eq 0 ]; then + echo "All ${checked} checked tools are present and runnable." +else + echo "Missing or non-functional tools (${#missing[@]} of ${checked}):" >&2 + for tool in "${missing[@]}"; do + echo " - ${tool}" >&2 + done + exit 1 +fi diff --git a/nix/docker/install-sanitizer-libs.sh b/bin/install-sanitizer-libs.sh similarity index 100% rename from nix/docker/install-sanitizer-libs.sh rename to bin/install-sanitizer-libs.sh diff --git a/cspell.config.yaml b/cspell.config.yaml index 77f0e9df7a..0d38c4be7b 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -109,6 +109,7 @@ words: - enabled - enablerepo - endmacro + - envrc - exceptioned - EXPECT_STREQ - Falco diff --git a/docs/build/advanced_conan.md b/docs/build/advanced_conan.md new file mode 100644 index 0000000000..aae17e385a --- /dev/null +++ b/docs/build/advanced_conan.md @@ -0,0 +1,193 @@ +# Advanced Conan configuration + +This document provides advanced instructions for setting up and configuring Conan for `xrpld` development: custom profiles, the lockfile, patched recipes, and profile tweaks. + +## Custom profile + +If the default profile does not work for you and you do not yet have a Conan +profile, you can create one by running: + +```bash +conan profile detect +``` + +You may need to make changes to the profile to suit your environment. You can +refer to the provided `conan/profiles/default` profile for inspiration, and you +may also need to apply the required [tweaks](#conan-profile-tweaks) to this +default profile. + +## Conan lockfile + +To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html), +which has to be updated every time dependencies change. + +Please see the [instructions on how to regenerate the lockfile](../../conan/lockfile/README.md). + +## Patched recipes + +Occasionally, we need patched recipes or recipes not present in Conan Center. +We maintain a fork of the Conan Center Index +[here](https://github.com/XRPLF/conan-center-index/) containing the modified and newly added recipes. + +To ensure our patched recipes are used, you must add our Conan remote at a +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 +``` + +Alternatively, you can pull our recipes from the repository and export them locally: + +```bash +# Define which recipes to export. +recipes=('abseil' 'ed25519' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi') + +# Selectively check out the recipes from our CCI fork. +cd external +mkdir -p conan-center-index +cd conan-center-index +git init +git remote add origin git@github.com:XRPLF/conan-center-index.git +git sparse-checkout init +for recipe in "${recipes[@]}"; do + echo "Checking out recipe '${recipe}'..." + git sparse-checkout add recipes/${recipe} +done +git fetch origin master +git checkout master + +./export_all.sh +cd ../../ +``` + +In the case we switch to a newer version of a dependency that still requires a +patch or add a new dependency, it will be necessary for you to pull in the changes and re-export the +updated dependencies with the newer version. However, if we switch to a newer +version that no longer requires a patch, no action is required on your part, as +the new recipe will be automatically pulled from the official Conan Center. + +> [!NOTE] +> You might need to add `--lockfile=""` to your `conan install` command +> to avoid automatic use of the existing `conan.lock` file when you run +> `conan export` manually on your machine +> +> This is not recommended though, as you might end up using different revisions of recipes. + +## Conan profile tweaks + +### Missing compiler version + +If you see an error similar to the following after running `conan profile show`: + +```text +ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value. +Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1', +'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15', +'15.0', '16', '16.0'] +Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting" +``` + +you need to create `$(conan config home)/settings_user.yml` file if it doesn't exist and add the required version number(s) +to the `version` array specific for your compiler. For example: + +```yaml +compiler: + apple-clang: + version: ["17.0"] +``` + +### Multiple compilers + +If you have multiple compilers installed, make sure to select the one to use in +your default Conan configuration **before** running `conan profile detect`, by +setting the `CC` and `CXX` environment variables. + +For example, if you are running MacOS and have [homebrew +LLVM@18](https://formulae.brew.sh/formula/llvm@18), and want to use it as a +compiler in the new Conan profile: + +```bash +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +conan profile detect +``` + +You should also explicitly set the path to the compiler in the profile file, +which helps to avoid errors when `CC` and/or `CXX` are set and disagree with the +selected Conan profile. For example: + +```text +[conf] +tools.build:compiler_executables={'c':'/usr/bin/gcc','cpp':'/usr/bin/g++'} +``` + +### Multiple profiles + +You can manage multiple Conan profiles in the directory +`$(conan config home)/profiles`, for example renaming `default` to a different +name and then creating a new `default` profile for a different compiler. + +### Select language + +The default profile created by Conan will typically select different C++ dialect +than C++23 used by this project. You should set `23` in the profile line +starting with `compiler.cppstd=`. For example: + +```bash +sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)/profiles/default +``` + +### Select standard library in Linux + +**Linux** developers will commonly have a default Conan [profile][] that +compiles with GCC and links with libstdc++. If you are linking with libstdc++ +(see profile setting `compiler.libcxx`), then you will need to choose the +`libstdc++11` ABI: + +```bash +sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan config home)/profiles/default +``` + +### Select architecture and runtime in Windows + +**Windows** developers may need to use the x64 native build tools. An easy way +to do that is to run the shortcut "x64 Native Tools Command Prompt" for the +version of Visual Studio that you have installed. + +Windows developers must also build `xrpld` and its dependencies for the x64 +architecture: + +```bash +sed -i.bak -e 's|^arch=.*$|arch=x86_64|' $(conan config home)/profiles/default +``` + +**Windows** developers also must select static runtime: + +```bash +sed -i.bak -e 's|^compiler\.runtime=.*$|compiler.runtime=static|' $(conan config home)/profiles/default +``` + +## Add a Dependency + +If you want to experiment with a new package, follow these steps: + +1. Search for the package on [Conan Center](https://conan.io/center/). +2. Modify [`conanfile.py`](../../conanfile.py): + - Add a version of the package to the `requires` property. + - Change any default options for the package by adding them to the + `default_options` property (with syntax `'$package:$option': $value`). +3. Regenerate the [Conan lockfile](../../conan/lockfile/README.md) so the new + dependency is captured: + + ```bash + ./conan/lockfile/regenerate.sh + ``` + +4. Modify [`CMakeLists.txt`](../../CMakeLists.txt): + - Add a call to `find_package($package REQUIRED)`. + - Link a library from the package to the target `xrpl_libs` + (search for the existing call to `target_link_libraries(xrpl_libs INTERFACE ...)`). +5. Start coding! Don't forget to include whatever headers you need from the package. + +[profile]: https://docs.conan.io/2/reference/config_files/profiles.html diff --git a/docs/build/conan.md b/docs/build/conan.md index 9dcd2c8f1c..22c25a0bf9 100644 --- a/docs/build/conan.md +++ b/docs/build/conan.md @@ -115,7 +115,7 @@ By default, Conan will use the profile named "default". [find_package]: https://cmake.org/cmake/help/latest/command/find_package.html [pcf]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-configuration-file [prefix_path]: https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html -[profile]: https://docs.conan.io/en/latest/reference/profiles.html +[profile]: https://docs.conan.io/2/reference/config_files/profiles.html [pvf]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-version-file [runtime]: https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_RUNTIME_LIBRARY.html [search]: https://cmake.org/cmake/help/latest/command/find_package.html#search-procedure diff --git a/docs/build/environment.md b/docs/build/environment.md index fb1ebde8bc..2cca608567 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -1,69 +1,73 @@ Our [build instructions][BUILD.md] assume you have a C++ development environment complete with Git, Python, Conan, CMake, and a C++ compiler. -This document exists to help readers set one up on any of the Big Three -platforms: Linux, macOS, or Windows. - -As an alternative to system packages, the Nix development shell can be used to provide a development environment. See [using nix development shell](./nix.md) for more details. +This document explains how to set one up. [BUILD.md]: ../../BUILD.md -## Linux +## Tested compiler versions -Package ecosystems vary across Linux distributions, -so there is no one set of instructions that will work for every Linux user. -The instructions below are written for Debian 12 (Bookworm). +`xrpld` is built in the **C++23** dialect by default. +Make sure your toolchain is recent enough — the compiler versions currently tested in CI are: -``` -export GCC_RELEASE=12 -sudo apt update -sudo apt install --yes gcc-${GCC_RELEASE} g++-${GCC_RELEASE} python3-pip \ - python-is-python3 python3-venv python3-dev curl wget ca-certificates \ - git build-essential cmake ninja-build libc6-dev -sudo pip install --break-system-packages conan +| Compiler | Version | +| ----------- | ------- | +| GCC | 15.2 | +| Clang | 22 | +| Apple Clang | 17 | +| MSVC | 19.44 | -sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-${GCC_RELEASE} 999 -sudo update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-${GCC_RELEASE} 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-${GCC_RELEASE} \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-${GCC_RELEASE} \ - --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-${GCC_RELEASE} \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-${GCC_RELEASE} \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-${GCC_RELEASE} \ - --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-${GCC_RELEASE} \ - --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-${GCC_RELEASE} \ - --slave /usr/bin/lto-dump lto-dump /usr/bin/lto-dump-${GCC_RELEASE} -sudo update-alternatives --auto cc -sudo update-alternatives --auto gcc +LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22. + +Older compilers may fail to build the latest `develop` code: the codebase now +relies on C++23 features and has been adjusted for `clang-tidy`. +If the latest code doesn't build for you, update your build toolchain first. + +## Linux and macOS + +The **recommended way** to get a development environment on Linux and macOS is +the Nix development shell. It provides the exact tooling used in CI — `git`, +`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else — +with a single command and without installing anything system-wide: + +```bash +nix --experimental-features 'nix-command flakes' develop ``` -If you use different Linux distribution, hope the instruction above can guide -you in the right direction. We try to maintain compatibility with all recent -compiler releases, so if you use a rolling distribution like e.g. Arch or CentOS -then there is a chance that everything will "just work". +On **Linux**, Nix also provides the compiler (GCC). On **macOS**, the shell uses +your **system-wide Apple Clang** as the compiler, so you still need to manage +its version (see below). -## macOS +See [Using the Nix development shell](./nix.md) for installation and usage +details, including how to select a different compiler. -Open a Terminal and enter the below command to bring up a dialog to install -the command line developer tools. -Once it is finished, this command should return a version greater than the -minimum required (see [BUILD.md][]). +> [!NOTE] +> Using Nix is not mandatory. Any custom environment (Homebrew packages or +> anything else) will continue to work, but then it is up to you to keep it in +> sync with the environment used in CI. Nix unifies the development environment +> for everyone and synchronizes updates, which is why we recommend it. -``` +### macOS: managing the Apple Clang version + +Because the Nix shell uses the system-wide Apple Clang on macOS, the compiler +version is whatever your installed Xcode (or Command Line Tools) provides. The +following command should return a version greater than or equal to the +[minimum required](#tested-compiler-versions): + +```bash clang --version ``` -### Install Xcode Specific Version (Optional) - -If you develop other applications using XCode you might be consistently updating to the newest version of Apple Clang. -This will likely cause issues building xrpld. You may want to install a specific version of Xcode: +If you develop other applications using Xcode, you might be consistently +updating to the newest version of Apple Clang, which will likely cause issues +building xrpld. You may want to install and pin a specific version of Xcode: 1. **Download Xcode** - Visit [Apple Developer Downloads](https://developer.apple.com/download/more/) - Sign in with your Apple Developer account - - Search for an Xcode version that includes **Apple Clang (Expected Version)** + - Search for an Xcode version that includes the expected Apple Clang version - Download the `.xip` file -2. **Install and Configure Xcode** +2. **Install and configure Xcode** ```bash # Extract the .xip file and rename for version management @@ -79,62 +83,28 @@ This will likely cause issues building xrpld. You may want to install a specific export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer ``` -The command line developer tools should include Git too: +## Windows -``` -git --version -``` +Nix is not available on Windows, so the required tools have to be installed +manually: -Install [Homebrew][], -use it to install [pyenv][], -use it to install Python, -and use it to install Conan: +- [Visual Studio 2022](https://visualstudio.microsoft.com/) with the + **"Desktop development with C++"** workload — this provides MSVC and the + "x64 Native Tools Command Prompt". +- [Git for Windows](https://git-scm.com/download/win) +- [Python 3.11](https://www.python.org/downloads/), or higher +- [Conan 2.17](https://conan.io/downloads.html), or higher +- [CMake 3.22](https://cmake.org/download/), or higher -[Homebrew]: https://brew.sh/ -[pyenv]: https://github.com/pyenv/pyenv - -``` -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -brew update -brew install xz -brew install pyenv -pyenv install 3.11 -pyenv global 3.11 -eval "$(pyenv init -)" -pip install 'conan' -``` - -Install CMake with Homebrew too: - -``` -brew install cmake -``` +> [!NOTE] +> Windows is used for development only and is not recommended for production. ## Clang-tidy -Clang-tidy is required to run static analysis checks locally (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). -It is not required to build the project. Currently this project uses clang-tidy version 21. +`clang-tidy` is required to run static analysis checks locally (see +[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the +project. This project currently uses `clang-tidy` version 22. -### Linux - -LLVM 21 is not available in the default Debian 12 (Bookworm) repositories. -Install it using the official LLVM apt installer: - -``` -wget https://apt.llvm.org/llvm.sh -chmod +x llvm.sh -sudo ./llvm.sh 21 -sudo apt install --yes clang-tidy-21 -``` - -Then use `run-clang-tidy-21` when running clang-tidy locally. - -### macOS - -Install LLVM 21 via Homebrew: - -``` -brew install llvm@21 -``` - -Then use `run-clang-tidy` from the LLVM 21 Homebrew prefix when running clang-tidy locally. +On Linux and macOS, the [Nix development shell](./nix.md) provides `clang-tidy` +22 out of the box — run it via `run-clang-tidy`. No separate installation is +needed. diff --git a/docs/build/nix.md b/docs/build/nix.md index 33bb3711d0..2ae483aefe 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -2,9 +2,12 @@ This guide explains how to use Nix to set up a reproducible development environment for xrpld. Using Nix eliminates the need to manually install utilities and ensures consistent tooling across different machines. +**The Nix development shell is the recommended way to develop xrpld.** It unifies the development environment for everyone and synchronizes updates: the same tooling and compiler versions are used both here and in CI. Any custom environment (Homebrew packages or anything else) will continue to work, but then it is up to you to keep it in sync with the environment used in CI. + ## Benefits of Using Nix - **Reproducible environment**: Everyone gets the same versions of tools and compilers +- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment - **No system pollution**: Dependencies are isolated and don't affect your system packages - **Multiple compiler versions**: Easily switch between different GCC and Clang versions - **Quick setup**: Get started with a single command @@ -28,11 +31,22 @@ This will: - Download and set up all required development tools (CMake, Ninja, Conan, etc.) - Configure the appropriate compiler for your platform: - - **macOS**: Apple Clang (default system compiler) - - **Linux**: GCC 15 + - **Linux**: GCC 15.2 (provided by Nix) + - **macOS**: Apple Clang (your system compiler) The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster. +### Platform notes + +- **Linux**: `nix develop` gives you a shell with all the tooling necessary to + develop xrpld and with GCC 15.2 (also provided by Nix). There are no caveats. +- **macOS**: `nix develop` gives you a full environment too. The compiler is + your system-wide Apple Clang, while every other tool — including Conan — is + provided by Nix. Conan has no binary in the Nix cache for macOS, so it is + built from source the first time you enter the shell, which makes the initial + setup slower (this is handled automatically; see + [`nix/devshell.nix`](../../nix/devshell.nix)). + > [!TIP] > To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`: > @@ -51,7 +65,7 @@ The first time you run this command, it will take a few minutes to download and A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`. Use `nix flake show` to see all the available development shells. -Use `nix develop .#no_compiler` to use the compiler from your system. +Use `nix develop .#no-compiler` to use the compiler from your system. ### Example Usage @@ -68,12 +82,28 @@ nix develop ### Using a different shell -`nix develop` opens bash by default. If you want to use another shell this could be done by adding `-c` flag. For example: +`nix develop` opens bash by default. To use another shell, pass it with the `-c` flag — this works with any shell, e.g. `zsh` or `fish`: ```bash +# Use zsh nix develop -c zsh + +# Use fish +nix develop -c fish + +# Use your login shell +nix develop -c "$SHELL" ``` +> [!WARNING] +> Your shell's interactive startup files (e.g. `config.fish`, `.zshrc`) may prepend other directories — most commonly Homebrew — to `$PATH`, which can shadow the tools provided by the Nix shell. After entering, verify that tools resolve into the Nix store: +> +> ```bash +> command -v cmake # should print a /nix/store/... path +> ``` +> +> If it doesn't, either adjust your shell configuration so it doesn't override `$PATH`, or use [direnv](#automatic-activation-with-direnv) (below), which loads the environment _after_ your shell config and so takes precedence regardless of the shell you use. + ## Building xrpld with Nix Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). @@ -82,6 +112,8 @@ Once inside the Nix development shell, follow the standard [build instructions]( [direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory. +This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`. To use it, install direnv for your shell, then add an `.envrc` containing `use flake` at the repository root and run `direnv allow`. + ## Conan and Prebuilt Packages Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source: @@ -93,3 +125,8 @@ conan install .. --output-folder . --build '*' --settings build_type=Release ## Updating `flake.lock` file To update `flake.lock` to the latest revision use `nix flake update` command. + +## Troubleshooting + +See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues, +such as `nix develop` failing inside Git worktrees. diff --git a/docs/build/nix_troubleshooting.md b/docs/build/nix_troubleshooting.md new file mode 100644 index 0000000000..ae5cb8059a --- /dev/null +++ b/docs/build/nix_troubleshooting.md @@ -0,0 +1,61 @@ +# Troubleshooting Nix problems + +Common issues encountered when using the [Nix development shell](./nix.md), and +how to resolve them. + +## Git worktrees + +If `nix develop` fails with an error like: + +``` +error: + … while fetching the input 'git+file:///path/to/rippled' + + error: opening Git repository "/path/to/rippled": unsupported extension name extensions.relativeworktrees (libgit2 error code = 6) +``` + +then your Nix is linked against a libgit2 older than **1.9.4**. Git 2.48+ writes +the `extensions.relativeWorktrees` config entry when a worktree is created with +relative paths (`git worktree add --relative-paths`, or with +`worktree.useRelativePaths=true`), and older libgit2 versions refuse to open a +repository that uses it. Nix uses libgit2 to read the flake, so evaluation +fails. + +> [!IMPORTANT] +> This entry is written to the **shared** repository config, so once any +> relative worktree exists, `nix develop` fails in the main checkout too — not +> just inside the worktree. + +### Workarounds + +These work today, with any Nix version: + +- bypass libgit2 with a `path:` flakeref: `nix develop "path:$PWD"` + (note: this copies the working tree to the store and ignores `.gitignore`); or +- create worktrees with absolute paths (omit `--relative-paths`); or +- clear the extension if you don't need relative worktrees: + `git config --unset extensions.relativeWorktrees`. + +### Permanent fix + +The fix is in [libgit2 1.9.4](https://github.com/libgit2/libgit2/releases/tag/v1.9.4), +so the real solution is a Nix that links against libgit2 `1.9.4` or newer. Check +which version yours links against: + +```bash +nix-store -qR "$(readlink -f "$(command -v nix)")" | grep libgit2 +``` + +> [!WARNING] +> `nix upgrade-nix` does **not** help yet. It installs the build from the +> official [`nix-fallback-paths`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/tools/nix-fallback-paths.nix), +> which is still linked against libgit2 `1.9.2` — there is no new upstream Nix +> release with the fix. (On some systems that build is even the exact store path +> you already have, making the upgrade a no-op.) + +nixpkgs has already rebuilt Nix against the fixed libgit2 (e.g. `nix-2.34.7+1`), +so the cleanest path is to reinstall Nix using your usual installation method +once it picks up that rebuild, then re-run the `grep libgit2` check above to +confirm it reports `1.9.4` or newer. + +Until then, prefer the workarounds above. diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index e6df48e18c..6d8980f897 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -71,7 +71,7 @@ if [ ! -e "${target}" ]; then fi EOF -COPY nix/docker/check-tools.sh /tmp/check-tools.sh +COPY bin/check-tools.sh /tmp/check-tools.sh RUN /tmp/check-tools.sh # Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones. @@ -93,7 +93,7 @@ RUN if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then \ SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] # Sanity-check that the built binaries run correctly in the vanilla base image, with the necessary sanitizer runtime libraries installed. -COPY nix/docker/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh +COPY bin/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh COPY nix/docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh COPY --from=final /tmp/bins /tmp/bins diff --git a/nix/docker/README.md b/nix/docker/README.md new file mode 100644 index 0000000000..085433b758 --- /dev/null +++ b/nix/docker/README.md @@ -0,0 +1,90 @@ +# Nix CI Docker images + +This directory builds the Docker images used by xrpld's Linux CI. Each image +bundles the **exact same toolchain that the Nix development shell provides** +(see [`docs/build/nix.md`](../../docs/build/nix.md)), so what runs in CI matches +what developers get locally from `nix develop`. + +The toolchain (CMake, Ninja, Conan, GCC, Clang, clang-tidy, the +sanitizer/coverage tools, …) is defined in [`nix/packages.nix`](../packages.nix) +and assembled for CI by [`nix/ci-env.nix`](../ci-env.nix). The Docker build +turns that Nix environment into an ordinary container image layered on top of a +conventional base image (Ubuntu, Debian, RHEL, or `nixos/nix`). + +## Images + +The images are built by the [`build-nix-images.yml`](../../.github/workflows/build-nix-images.yml) +workflow and pushed to `ghcr.io/xrplf/xrpld/nix-`. The `` is +selected through the `BASE_IMAGE` build argument; the base images are the +**oldest supported version** of each distribution we target: + +| Image | `BASE_IMAGE` | Notes | +| ------------ | -------------------------------------------- | -------------------------------------------------- | +| `nix-nixos` | `nixos/nix:latest` | Build/lint only; binaries are not run (see below). | +| `nix-ubuntu` | `ubuntu:20.04` | Oldest supported Ubuntu (glibc 2.31). | +| `nix-debian` | `debian:bookworm` | | +| `nix-rhel` | `registry.access.redhat.com/ubi9/ubi:latest` | | + +All images carry the full toolchain on `PATH` (via `/nix/ci-env/bin`) plus the +CA bundle shipped in the Nix environment, so HTTPS clients (git, curl, Conan) +work without `ca-certificates` being installed in the base image. + +## Build stages + +[`Dockerfile`](./Dockerfile) is a multi-stage build: + +1. **`builder`** — On a `nixos/nix` builder, evaluate the flake and build the + CI environment (`nix/ci-env.nix`). The resulting Nix store closure (the + complete set of store paths the toolchain depends on) is copied into a + staging directory. +2. **`final`** — Start from `BASE_IMAGE`, copy in the Nix store closure and the + `ci-env` symlink tree, and wire up `PATH` and the CA bundle. It then: + - installs the dynamic linker if the base image lacks one (see + [How libc is handled](#how-libc-is-handled)), + - runs [`bin/check-tools.sh`](../../bin/check-tools.sh) to verify every + expected tool is present and runnable, and + - compiles the C++ test programs in + [`test_files/`](./test_files) with both `g++` and `clang++`, and sanitizers. +3. **`tester`** — Start again from a clean `BASE_IMAGE` (no Nix toolchain), + install only the sanitizer runtime libraries + ([`install-sanitizer-libs.sh`](./install-sanitizer-libs.sh)), and run the + binaries compiled in `final`. This proves the binaries built with the Nix + toolchain actually run on a vanilla base image. On `nixos/nix` this step is + skipped (the binaries are patched for a conventional FHS loader). +4. **Output** — The final image is gated on the tester succeeding: it copies a + sentinel file out of `tester`, so a failed test run fails the whole build. + +## How libc is handled + +The goal is for binaries built in these images to run on the **oldest supported +base image** (Ubuntu 20.04, glibc 2.31) and newer — without the developer's Nix +toolchain being present at runtime. Two pieces make that work: + +- **Compilers linked against an old glibc.** The Nix CI environment does not use + nixpkgs' current glibc. Instead it pins a 2020 nixpkgs snapshot whose primary + glibc is **2.31** (matching Ubuntu 20.04), via the `nixpkgs-custom-glibc` + flake input. GCC, Clang, binutils and compiler-rt are all rebuilt/wrapped + against this custom glibc (see [`nix/ci-env.nix`](../ci-env.nix)). As a result + the libraries they emit (`libstdc++`, `libgcc_s`, the sanitizer runtimes) + reference only symbols available in glibc 2.31. + +- **An expected dynamic linker in the image.** + Binaries built in Nix environments reference a dynamic linker from Nix store paths, which won't be present in the base image. However, + [`loader-path.sh`](./loader-path.sh) reports the expected loader path for the + current architecture, so we can patch the binaries to use the correct loader. + +The build then verifies all of this end to end: the test programs in +`test_files/` (a regular binary plus ASan/TSan/UBSan variants) are compiled in +`final`, their `PT_INTERP` is patched to the target loader, and they are run in +the clean `tester` stage to confirm each emits the expected sanitizer +diagnostic on a stock base image. + +## Files + +| File | Purpose | +| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. | +| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. | +| [`./test_files/`](./test_files) | C++ sources and scripts to compile and run the sanitizer smoke tests. | +| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. | +| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. | diff --git a/nix/docker/check-tools.sh b/nix/docker/check-tools.sh deleted file mode 100755 index a46c2dd997..0000000000 --- a/nix/docker/check-tools.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Verify that every tool expected in the Nix CI env is present and runnable. -set -euo pipefail - -ccache --version -clang --version -clang++ --version -clang-format --version -ClangBuildAnalyzer --version -cmake --version -conan --version -curl --version -doxygen --version -file --version -g++ --version -gcc --version -gcov --version -gcovr --version -gh --version -git --version -git-cliff --version -gpg --version -less --version -make --version -mold --version -netstat --version -ninja --version -perl --version -pkg-config --version -pre-commit --version -python3 --version -run-clang-tidy --help -vim --version - -# A simple test to verify that git can clone a repository over HTTPS -# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up. -tmp_clone="$(mktemp -d)" -git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions" -rm -rf "${tmp_clone}" From 7b9d55326db2fc9f5da4d16f22985c4670af8082 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 16 Jun 2026 18:35:33 +0100 Subject: [PATCH 025/121] build: Add zip to Nix images (#7551) --- nix/packages.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/packages.nix b/nix/packages.nix index 5a7f20ec49..612b1cb215 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -33,5 +33,6 @@ in python3 runClangTidy vim + zip ]; } From 45ddc1d868cad0aa1ec9fedbdff3d4eaf882eb72 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 17 Jun 2026 00:13:33 +0100 Subject: [PATCH 026/121] build: Add git-lfs to Nix images (#7561) --- nix/packages.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/packages.nix b/nix/packages.nix index 612b1cb215..79de8fac89 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -19,6 +19,7 @@ in gh git git-cliff + git-lfs gnumake gnupg # needed for signing commits & codecov/codecov-action llvmPackages_22.clang-tools From 5de434436e339bfdd91b31319ca87ff8329a3465 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 17 Jun 2026 11:02:17 +0100 Subject: [PATCH 027/121] ci: Make clang-tidy workflow adjustments to stay in sync with Clio (#7563) --- .github/workflows/reusable-clang-tidy.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 34fa860e12..5d2325cb1d 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -20,9 +20,12 @@ env: BUILD_DIR: build BUILD_TYPE: Debug # Debug so that ASSERTS and such participate in clang-tidy check - OUTPUT_FILE: clang-tidy-output.txt - DIFF_FILE: clang-tidy-git-diff.txt - ISSUE_FILE: clang-tidy-issue.md + OUTPUT_FILE: /tmp/clang-tidy-output.txt + FILTERED_OUTPUT_FILE: /tmp/clang-tidy-filtered-output.txt + DIFF_FILE: /tmp/clang-tidy-git-diff.txt + ISSUE_FILE: /tmp/clang-tidy-issue.md + + COMPILER: clang jobs: determine-files: @@ -59,7 +62,7 @@ jobs: - name: Set compiler environment uses: ./.github/actions/set-compiler-env with: - compiler: clang + compiler: ${{ env.COMPILER }} - name: Setup Conan uses: ./.github/actions/setup-conan @@ -150,21 +153,21 @@ jobs: run: | if [ -f "${OUTPUT_FILE}" ]; then # Extract lines containing 'error:', 'warning:', or 'note:' - grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >filtered-output.txt || true + grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true # If filtered output is empty, use original (might be a different error format) - if [ ! -s filtered-output.txt ]; then - cp "${OUTPUT_FILE}" filtered-output.txt + if [ ! -s "${FILTERED_OUTPUT_FILE}" ]; then + cp "${OUTPUT_FILE}" "${FILTERED_OUTPUT_FILE}" fi # Truncate if too large - head -c 60000 filtered-output.txt >>"${ISSUE_FILE}" - if [ "$(wc -c >"${ISSUE_FILE}" + if [ "$(wc -c <"${FILTERED_OUTPUT_FILE}")" -gt 60000 ]; then echo "" >>"${ISSUE_FILE}" echo "... (output truncated, see artifacts for full output)" >>"${ISSUE_FILE}" fi - rm filtered-output.txt + rm "${FILTERED_OUTPUT_FILE}" else echo "No output file found" >>"${ISSUE_FILE}" fi From 044ca7719dda036f1950e599cd636ce8276eb67e Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:58:01 +0100 Subject: [PATCH 028/121] release: Bump version to 3.3.0-b0 Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index c488bb20de..820936a22d 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0" +char const* const versionString = "3.3.0-b0" // clang-format on ; From 7e0ff536f55b684f6c8e73028c20758c03e334bc Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:31:04 +0100 Subject: [PATCH 029/121] refactor: Rerevert "Explicitly trim the heap after cache sweeps (#6022)" Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- src/xrpld/app/main/Application.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index af32cf0ba6..67b5e30eb7 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -1088,6 +1089,8 @@ public: << "; size after: " << cachedSLEs_.size(); } + mallocTrim("doSweep", journal_); + // Set timer to do another sweep later. setSweepTimer(); } From cb2642be050b2683c929d03ab10d85c6cb01f933 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 17 Jun 2026 14:54:46 +0100 Subject: [PATCH 030/121] build: Add graphviz to Nix images (#7566) --- nix/packages.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/packages.nix b/nix/packages.nix index 79de8fac89..6202168733 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -22,6 +22,7 @@ in git-lfs gnumake gnupg # needed for signing commits & codecov/codecov-action + graphviz llvmPackages_22.clang-tools less # needed for git diff mold From f07de6c4540e91ed312404bb8f511fdc562ea9b5 Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Wed, 17 Jun 2026 06:54:55 -0700 Subject: [PATCH 031/121] ci: Disable assertions on Release builds (#7443) --- .github/scripts/strategy-matrix/generate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index 6353567f27..a269cb25d4 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -20,8 +20,6 @@ _SANITIZER_SUFFIX: dict[str, str] = { def get_cmake_args(build_type: str, extra_args: str) -> str: """Get the full list of CMake arguments for a config.""" args = _BASE_CMAKE_ARGS.copy() - if build_type == "Release": - args.append("-Dassert=ON") if extra_args: args.extend(extra_args.split()) return " ".join(args) From 480676d0bf9ec7dc6b35267c63dfd162746b5d3b Mon Sep 17 00:00:00 2001 From: solunolab Date: Wed, 17 Jun 2026 21:55:00 +0800 Subject: [PATCH 032/121] docs: Fix some comments to improve readability (#7405) Signed-off-by: solunolab Co-authored-by: Ayaz Salikhov --- include/xrpl/basics/sanitizers.h | 2 +- include/xrpl/ledger/helpers/CredentialHelpers.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/xrpl/basics/sanitizers.h b/include/xrpl/basics/sanitizers.h index b954952848..7937344b53 100644 --- a/include/xrpl/basics/sanitizers.h +++ b/include/xrpl/basics/sanitizers.h @@ -4,7 +4,7 @@ /* ASAN flags some false positives with sudden jumps in control flow, like exceptions, or when encountering coroutine stack switches. This macro can be used to disable ASAN - intrumentation for specific functions. + instrumentation for specific functions. */ #if defined(__GNUC__) || defined(__clang__) #define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress"))) diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index 549644764f..0cfbbde538 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -36,13 +36,13 @@ checkFields(STTx const& tx, beast::Journal j); TER valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j); -// Check if subject has any credential maching the given domain. If you call it +// Check if subject has any credential matching the given domain. If you call it // in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in // doApply. This will ensure that expired credentials are deleted. TER validDomain(ReadView const& view, uint256 domainID, AccountID const& subject); -// This function is only called when we about to return tecNO_PERMISSION +// This function is only called when we are about to return tecNO_PERMISSION // because all the checks for the DepositPreauth authorization failed. TER authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst); @@ -58,7 +58,7 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j); } // namespace credentials -// Check expired credentials and for credentials maching DomainID of the ledger +// Check expired credentials and for credentials matching DomainID of the ledger // object TER verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j); From 772ea80a25227b5e9fb75ea0cdb62723b099cd33 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Wed, 17 Jun 2026 19:20:54 -0400 Subject: [PATCH 033/121] fix: Use template for granular delegation permissions (#6613) Co-authored-by: Bart --- include/xrpl/ledger/helpers/DelegateHelpers.h | 10 +- include/xrpl/protocol/Permissions.h | 80 +++- include/xrpl/protocol/detail/features.macro | 2 +- .../xrpl/protocol/detail/permissions.macro | 89 ++-- include/xrpl/tx/Transactor.h | 63 ++- .../xrpl/tx/transactors/account/AccountSet.h | 3 - include/xrpl/tx/transactors/payment/Payment.h | 5 +- .../tx/transactors/token/MPTokenIssuanceSet.h | 3 - include/xrpl/tx/transactors/token/TrustSet.h | 5 +- src/libxrpl/protocol/Permissions.cpp | 317 +++++++++----- src/libxrpl/protocol/STTx.cpp | 2 +- src/libxrpl/tx/Transactor.cpp | 36 +- src/libxrpl/tx/applySteps.cpp | 3 +- .../tx/transactors/account/AccountSet.cpp | 51 --- .../tx/transactors/delegate/DelegateUtils.cpp | 12 +- .../tx/transactors/payment/Payment.cpp | 30 +- .../transactors/token/MPTokenIssuanceSet.cpp | 36 -- src/libxrpl/tx/transactors/token/TrustSet.cpp | 44 +- src/test/app/Delegate_test.cpp | 407 +++++++++++++++++- 19 files changed, 851 insertions(+), 347 deletions(-) diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h index 9cdad7173d..a517eefdaa 100644 --- a/include/xrpl/ledger/helpers/DelegateHelpers.h +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h @@ -23,13 +23,9 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx); * @param delegate The delegate account. * @param type Used to determine which granted granular permissions to load, * based on the transaction type. - * @param granularPermissions Granted granular permissions tied to the - * transaction type. + * @return the granted granular permissions tied to the transaction type. */ -void -loadGranularPermission( - SLE::const_ref delegate, - TxType const& type, - std::unordered_set& granularPermissions); +std::unordered_set +getGranularPermission(SLE::const_ref delegate, TxType const& type); } // namespace xrpl diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 5d56fa4461..eb161ef7ad 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -7,8 +7,13 @@ #include #include #include +#include +#include namespace xrpl { + +class STTx; + /** * We have both transaction type permissions and granular type permissions. * Since we will reuse the TransactionFormats to parse the Transaction @@ -19,15 +24,15 @@ namespace xrpl { // Macro-generated, complex // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum GranularPermissionType : std::uint32_t { -#pragma push_macro("PERMISSION") -#undef PERMISSION +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION -#define PERMISSION(type, txType, value) type = (value), +#define GRANULAR_PERMISSION(name, txType, value, ...) name = (value), #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; // Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor @@ -40,15 +45,30 @@ class Permission private: Permission(); - std::unordered_map txFeatureMap_; + struct GranularPermissionEntry + { + std::string name; + TxType txType; + std::uint32_t permittedFlags; + SOTemplate permittedFields; - std::unordered_map delegableTx_; + GranularPermissionEntry( + std::string name, + TxType txType, + std::uint32_t permittedFlags, + std::vector fields); + }; - std::unordered_map granularPermissionMap_; + struct TxDelegationEntry + { + uint256 amendment; + Delegation delegable{NotDelegable}; + }; - std::unordered_map granularNameMap_; - - std::unordered_map granularTxTypeMap_; + std::unordered_set granularTxTypes_; + std::unordered_map txDelegationMap_; + std::unordered_map granularPermissionsByName_; + std::unordered_map granularPermissions_; public: static Permission const& @@ -59,30 +79,52 @@ public: operator=(Permission const&) = delete; [[nodiscard]] std::optional - getPermissionName(std::uint32_t const value) const; + getPermissionName(std::uint32_t value) const; [[nodiscard]] std::optional getGranularValue(std::string const& name) const; [[nodiscard]] std::optional - getGranularName(GranularPermissionType const& value) const; + getGranularName(GranularPermissionType value) const; [[nodiscard]] std::optional - getGranularTxType(GranularPermissionType const& gpType) const; + getGranularTxType(GranularPermissionType gpType) const; + // Returns a reference to avoid copying uint256 - 32 bytes. std::optional + // cannot hold references directly, so std::reference_wrapper is used. [[nodiscard]] std::optional> getTxFeature(TxType txType) const; [[nodiscard]] bool - isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const; + isDelegable(std::uint32_t permissionValue, Rules const& rules) const; + + [[nodiscard]] bool + hasGranularPermissions(TxType txType) const; // for tx level permission, permission value is equal to tx type plus one - static uint32_t - txToPermissionType(TxType const& type); + [[nodiscard]] static uint32_t + txToPermissionType(TxType type); // tx type value is permission value minus one - static TxType - permissionToTxType(uint32_t const& value); + [[nodiscard]] static TxType + permissionToTxType(std::uint32_t value); + + /** + * @brief Verifies a delegated transaction against its granular permission template. + * + * @note WARNING: Do not move this check before standard transaction-level + * format checks, which is in preclaim. This function assumes the transaction's + * base structural integrity (fees, sequence, signatures) has already been + * validated. + * + * @param tx The transaction to verify. + * @param heldPermissions The granular permissions that the sender hold. + * @return true if the transaction fields and flags comply with the granular template. + */ + [[nodiscard]] bool + checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const; }; } // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index d3500ab144..d25b0b1f2c 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -21,7 +21,7 @@ XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultN 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::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (IncludeKeyletFields, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicMPT, Supported::No, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/permissions.macro b/include/xrpl/protocol/detail/permissions.macro index 729861a013..35532a03ca 100644 --- a/include/xrpl/protocol/detail/permissions.macro +++ b/include/xrpl/protocol/detail/permissions.macro @@ -1,49 +1,74 @@ -#if !defined(PERMISSION) -#error "undefined macro: PERMISSION" +#if !defined(GRANULAR_PERMISSION) +#error "undefined macro: GRANULAR_PERMISSION" #endif /** - * PERMISSION(name, type, txType, value) + * GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields) * - * This macro defines a permission: - * name: the name of the permission. - * type: the GranularPermissionType enum. - * txType: the corresponding TxType for this permission. - * value: the uint32 numeric value for the enum type. + * Defines a granular permission: + * name: the granular permission name. + * txType: the corresponding TxType for this permission. + * value: the uint32 numeric value for the enum type. + * allowedFlags: transaction flags permitted under this permission. + * allowedFields: transaction fields permitted under this permission. */ -/** This permission grants the delegated account the ability to authorize a trustline. */ -PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537) +/** Grants the ability to authorize a trustline. */ +GRANULAR_PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537, tfUniversal | tfSetfAuth, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to freeze a trustline. */ -PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538) +/** Grants the ability to freeze a trustline. */ +GRANULAR_PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538, tfUniversal | tfSetFreeze, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to unfreeze a trustline. */ -PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539) +/** Grants the ability to unfreeze a trustline. */ +GRANULAR_PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539, tfUniversal | tfClearFreeze, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to set Domain. */ -PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540) +/** Grants the ability to set Domain. */ +GRANULAR_PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540, tfUniversal, + ({{sfDomain, SoeOptional}})) -/** This permission grants the delegated account the ability to set EmailHashSet. */ -PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541) +/** Grants the ability to set EmailHash. */ +GRANULAR_PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541, tfUniversal, + ({{sfEmailHash, SoeOptional}})) -/** This permission grants the delegated account the ability to set MessageKey. */ -PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542) +/** Grants the ability to set MessageKey. */ +GRANULAR_PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542, tfUniversal, + ({{sfMessageKey, SoeOptional}})) -/** This permission grants the delegated account the ability to set TransferRate. */ -PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543) +/** Grants the ability to set TransferRate. */ +GRANULAR_PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543, tfUniversal, + ({{sfTransferRate, SoeOptional}})) -/** This permission grants the delegated account the ability to set TickSize. */ -PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544) +/** Grants the ability to set TickSize. */ +GRANULAR_PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544, tfUniversal, + ({{sfTickSize, SoeOptional}})) -/** This permission grants the delegated account the ability to mint payment, which means sending a payment for a currency where the sending account is the issuer. */ -PERMISSION(PaymentMint, ttPAYMENT, 65545) +/** Grants the ability to mint payment (sending account is the issuer). Cross-currency payments are disallowed. */ +GRANULAR_PERMISSION(PaymentMint, ttPAYMENT, 65545, tfUniversal, + ({{sfDestination, SoeRequired}, + {sfAmount, SoeRequired}, + {sfSendMax, SoeOptional}, + {sfInvoiceID, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}})) -/** This permission grants the delegated account the ability to burn payment, which means sending a payment for a currency where the destination account is the issuer */ -PERMISSION(PaymentBurn, ttPAYMENT, 65546) +/** Grants the ability to burn payment (destination account is the issuer). Cross-currency payments are disallowed. */ +GRANULAR_PERMISSION(PaymentBurn, ttPAYMENT, 65546, tfUniversal, + ({{sfDestination, SoeRequired}, + {sfAmount, SoeRequired}, + {sfSendMax, SoeOptional}, + {sfInvoiceID, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}})) -/** This permission grants the delegated account the ability to lock MPToken. */ -PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547) +/** Grants the ability to lock an MPToken. */ +GRANULAR_PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547, tfUniversal | tfMPTLock, + ({{sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}})) -/** This permission grants the delegated account the ability to unlock MPToken. */ -PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548) +/** Grants the ability to unlock an MPToken. */ +GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock, + ({{sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}})) diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 86b1e856b3..a27d638107 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -222,8 +222,63 @@ public: return tesSUCCESS; } + /** + * This function can be overridden to introduce additional semantic constraints beyond the + * granular template validation for granular permissions. It is called by the base + * invokeCheckPermission method only after the transaction has successfully passed + * checkGranularSandbox. + */ static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) + { + return tesSUCCESS; + } + + /** + * Checks whether the transaction is authorized to be executed by the delegated account. + * This function enforces the strict permission check hierarchy. It is explicitly + * designed NOT to be overridden. Derived transactors must instead implement + * checkGranularSemantics to add custom validation logic for granular permissions. + * + * The evaluation proceeds as follows: + * - If transaction-level permission is granted, the function immediately returns tesSUCCESS. + * - If transaction-level permission is not granted, the function checks whether the transaction + * matches the granular permission template defined in permissions.macro. If it does, it then + * calls checkGranularSemantics to perform any additional, fine-grained validation. + * + */ + template + static NotTEC + invokeCheckPermission(ReadView const& view, STTx const& tx) + { + // heldGranularPermissions is passed by reference into checkPermission. + // It is populated with the sender’s granular permissions only when the sender + // lacks tx-level permission but has granular permissions that satisfy the + // granular permission template. + // + // - result is terNO_DELEGATE_PERMISSION: return immediately. + // - result is tesSUCCESS and heldGranularPermissions is empty: tx-level permission was + // granted, so we returned success before populating it. + // - result is tesSUCCESS and heldGranularPermissions is not empty: tx-level permission was + // not granted, but the held granular permissions passed checkGranularSandbox, so we proceed + // to checkGranularSemantics. + // + // WARNING: Do not simplify checkPermission to return only + // heldGranularPermissions or the ter code. Both the result and the + // populated set are required to enforce the strict permission hierarchy + // described above. + std::unordered_set heldGranularPermissions; + if (NotTEC const result = checkPermission(view, tx, heldGranularPermissions); + !isTesSuccess(result) || heldGranularPermissions.empty()) + { + return result; + } + + return T::checkGranularSemantics(view, tx, heldGranularPermissions); + } ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -353,6 +408,12 @@ protected: unit::ValueUnit min = unit::ValueUnit{}); private: + static NotTEC + checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions); + std::pair reset(XRPAmount fee); diff --git a/include/xrpl/tx/transactors/account/AccountSet.h b/include/xrpl/tx/transactors/account/AccountSet.h index a40a9ec963..91b38e7968 100644 --- a/include/xrpl/tx/transactors/account/AccountSet.h +++ b/include/xrpl/tx/transactors/account/AccountSet.h @@ -23,9 +23,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/payment/Payment.h b/include/xrpl/tx/transactors/payment/Payment.h index 14897b4efe..dd792aa1c2 100644 --- a/include/xrpl/tx/transactors/payment/Payment.h +++ b/include/xrpl/tx/transactors/payment/Payment.h @@ -32,7 +32,10 @@ public: preflight(PreflightContext const& ctx); static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions); static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h index 6a6d1fc445..428c573e2f 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h @@ -22,9 +22,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/token/TrustSet.h b/include/xrpl/tx/transactors/token/TrustSet.h index dcf454bea1..d719f06326 100644 --- a/include/xrpl/tx/transactors/token/TrustSet.h +++ b/include/xrpl/tx/transactors/token/TrustSet.h @@ -21,7 +21,10 @@ public: preflight(PreflightContext const& ctx); static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index ce3baeb35e..3aa9705b03 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -1,91 +1,136 @@ #include #include +#include #include -#include // IWYU pragma: keep #include +#include +#include +#include +#include // IWYU pragma: keep #include +#include #include #include #include +#include #include +#include +#include +#include +#include namespace xrpl { +Permission::GranularPermissionEntry::GranularPermissionEntry( + std::string name, + TxType txType, + std::uint32_t permittedFlags, + std::vector permittedFields) + : name(std::move(name)) + , txType(txType) + , permittedFlags(permittedFlags) + , permittedFields(std::move(permittedFields), TxFormats::getCommonFields()) +{ +} + Permission::Permission() { - txFeatureMap_ = { -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, amendment, ...) {value, amendment}, - -#include - -#undef TRANSACTION -#pragma pop_macro("TRANSACTION") - }; - - delegableTx_ = { -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, ...) {value, delegable}, - -#include - -#undef TRANSACTION -#pragma pop_macro("TRANSACTION") - }; - - granularPermissionMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION - -#define PERMISSION(type, txType, value) {#type, type}, - -#include - -#undef PERMISSION -#pragma pop_macro("PERMISSION") - }; - - granularNameMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION - -#define PERMISSION(type, txType, value) {type, #type}, - -#include - -#undef PERMISSION -#pragma pop_macro("PERMISSION") - }; - - granularTxTypeMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION - -#define PERMISSION(type, txType, value) {type, txType}, - -#include - -#undef PERMISSION -#pragma pop_macro("PERMISSION") - }; - - XRPL_ASSERT( - txFeatureMap_.size() == delegableTx_.size(), - "xrpl::Permission : txFeatureMap_ and delegableTx_ must have same " - "size"); - - for ([[maybe_unused]] auto const& permission : granularPermissionMap_) { - XRPL_ASSERT( - permission.second > UINT16_MAX, - "xrpl::Permission::granularPermissionMap_ : granular permission " - "value must not exceed the maximum uint16_t value."); +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegable, amendment, ...) \ + txDelegationMap_[static_cast(value)] = {amendment, delegable}; + +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + } + + granularPermissionsByName_ = { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +#define GRANULAR_PERMISSION(type, ...) {#type, type}, + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + }; + + { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \ + granularPermissions_.emplace( \ + std::piecewise_construct, \ + std::forward_as_tuple(GranularPermissionType::type), \ + std::forward_as_tuple( \ + #type, txType, static_cast(flags), std::vector fields)); + // NOLINTEND(bugprone-macro-parentheses) + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + } + + if (granularPermissionsByName_.size() != granularPermissions_.size()) + { + // LCOV_EXCL_START + Throw( + "granularPermissionsByName_ and granularPermissions_ must have same size"); + // LCOV_EXCL_STOP + } + + for (auto const& [name, type] : granularPermissionsByName_) + { + if (type <= UINT16_MAX) + { + // LCOV_EXCL_START + Throw( + "Granular permission value must exceed the maximum uint16_t value: " + name); + // LCOV_EXCL_STOP + } + } + + for (auto const& [type, entry] : granularPermissions_) + granularTxTypes_.insert(entry.txType); + + // Validate that all fields listed in permissions.macro exist in the + // corresponding transaction type's format, catching typos at startup. + for (auto const& [type, entry] : granularPermissions_) + { + if (!txDelegationMap_.contains(entry.txType)) + { + // LCOV_EXCL_START + Throw("Invalid granular permission txType in txDelegationMap_"); + // LCOV_EXCL_STOP + } + + auto const* fmt = TxFormats::getInstance().findByType(entry.txType); + if (fmt == nullptr) + { + // LCOV_EXCL_START + Throw("Invalid granular permission txType"); + // LCOV_EXCL_STOP + } + + for (auto const& field : entry.permittedFields) + { + if (fmt->getSOTemplate().getIndex(field.sField()) == -1) + { + // LCOV_EXCL_START + Throw("Invalid granular permission field"); + // LCOV_EXCL_STOP + } + } } } @@ -97,8 +142,11 @@ Permission::getInstance() } std::optional -Permission::getPermissionName(std::uint32_t const value) const +Permission::getPermissionName(std::uint32_t value) const { + if (value == 0) + return std::nullopt; + auto const permissionValue = static_cast(value); if (auto const granular = getGranularName(permissionValue)) return granular; @@ -114,90 +162,131 @@ Permission::getPermissionName(std::uint32_t const value) const std::optional Permission::getGranularValue(std::string const& name) const { - auto const it = granularPermissionMap_.find(name); - if (it != granularPermissionMap_.end()) + auto const it = granularPermissionsByName_.find(name); + if (it != granularPermissionsByName_.end()) return static_cast(it->second); return std::nullopt; } std::optional -Permission::getGranularName(GranularPermissionType const& value) const +Permission::getGranularName(GranularPermissionType value) const { - auto const it = granularNameMap_.find(value); - if (it != granularNameMap_.end()) - return it->second; + auto const it = granularPermissions_.find(value); + if (it != granularPermissions_.end()) + return it->second.name; return std::nullopt; } std::optional -Permission::getGranularTxType(GranularPermissionType const& gpType) const +Permission::getGranularTxType(GranularPermissionType gpType) const { - auto const it = granularTxTypeMap_.find(gpType); - if (it != granularTxTypeMap_.end()) - return it->second; + auto const it = granularPermissions_.find(gpType); + if (it != granularPermissions_.end()) + return it->second.txType; return std::nullopt; } +bool +Permission::hasGranularPermissions(TxType txType) const +{ + return granularTxTypes_.contains(txType); +} + std::optional> Permission::getTxFeature(TxType txType) const { - auto const txFeaturesIt = txFeatureMap_.find(txType); + auto const it = txDelegationMap_.find(txType); XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "xrpl::Permissions::getTxFeature : tx exists in txFeatureMap_"); + it != txDelegationMap_.end(), + "xrpl::Permission::getTxFeature : tx exists in txDelegationMap_"); - if (txFeaturesIt->second == uint256{}) + if (it->second.amendment == uint256{}) return std::nullopt; - return txFeaturesIt->second; + + return std::optional{std::cref(it->second.amendment)}; } bool -Permission::isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const +Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const { - auto const granularPermission = - getGranularName(static_cast(permissionValue)); - if (granularPermission) + if (permissionValue == 0) + return false; // LCOV_EXCL_LINE + + auto const amendmentEnabled = [&rules](TxDelegationEntry const& entry) { + return entry.amendment == uint256{} || rules.enabled(entry.amendment); + }; + + // Granular permissions may authorize a limited subset of a tx type even + // when the full tx type is not delegable. They still require the + // underlying transaction amendment to be enabled. + if (auto const granularIt = + granularPermissions_.find(static_cast(permissionValue)); + granularIt != granularPermissions_.end()) { - // granular permissions are always allowed to be delegated - return true; + auto const txIt = txDelegationMap_.find(granularIt->second.txType); + return txIt != txDelegationMap_.end() && amendmentEnabled(txIt->second); } auto const txType = permissionToTxType(permissionValue); - auto const it = delegableTx_.find(txType); + auto const txIt = txDelegationMap_.find(txType); - if (it == delegableTx_.end()) - return false; - - auto const txFeaturesIt = txFeatureMap_.find(txType); - XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "xrpl::Permissions::isDelegable : tx exists in txFeatureMap_"); - - // Delegation is only allowed if the required amendment for the transaction - // is enabled. For transactions that do not require an amendment, delegation - // is always allowed. - if (txFeaturesIt->second != uint256{} && !rules.enabled(txFeaturesIt->second)) - return false; - - if (it->second == Delegation::NotDelegable) - return false; - - return true; + // Tx-level permissions require the transaction type itself to be delegable, and + // the corresponding amendment enabled. + return txIt != txDelegationMap_.end() && txIt->second.delegable != NotDelegable && + amendmentEnabled(txIt->second); } uint32_t -Permission::txToPermissionType(TxType const& type) +Permission::txToPermissionType(TxType const type) { return static_cast(type) + 1; } TxType -Permission::permissionToTxType(uint32_t const& value) +Permission::permissionToTxType(uint32_t value) { + XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0"); return static_cast(value - 1); } +bool +Permission::checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const +{ + // Build union of flags upfront to enable an early exit. Fields are not stored and + // grouped in advance to avoid heap allocation. + std::uint32_t unionFlags = 0; + for (auto const& gp : heldPermissions) + { + auto const it = granularPermissions_.find(gp); + if (it != granularPermissions_.end()) + unionFlags |= it->second.permittedFlags; + } + + // Check if flags are permitted + if ((tx.getFlags() & ~unionFlags) != 0) + return false; + + // Check if fields are permitted. Every present field must appear in at least one held + // permission's template. The common fields are included in the constructor. + for (auto const& field : tx) + { + if (field.getSType() == STI_NOTPRESENT) + continue; + + if (!std::ranges::any_of(heldPermissions, [&](auto const& gp) { + auto const it = granularPermissions_.find(gp); + return it != granularPermissions_.end() && + it->second.permittedFields.getIndex(field.getFName()) != -1; + })) + return false; + } + + return true; +} + } // namespace xrpl diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 55f0ea1289..be3b1a082f 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -217,7 +217,7 @@ 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::checkPermission` + // enforced in `Transactor::invokeCheckPermission` // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) if (isFieldPresent(sfDelegate)) return getAccountID(sfDelegate); diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index aa7b81c015..b57d30d2b3 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include @@ -175,6 +177,16 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx[sfDelegate] == ctx.tx[sfAccount]) return temBAD_SIGNER; + + auto const& perm = Permission::getInstance(); + auto const txType = ctx.tx.getTxnType(); + + // If the transaction is not delegable and does not have granular permissions, fail earlier + // with temINVALID. This is to prevent transactions that are not delegable at all from + // being processed further in the invokeCheckPermission function. + if (!perm.isDelegable(Permission::txToPermissionType(txType), ctx.rules) && + !perm.hasGranularPermissions(txType)) + return temINVALID; } if (auto const ret = preflight0(ctx, flagMask)) @@ -295,19 +307,33 @@ Transactor::preflightSigValidated(PreflightContext const& ctx) } NotTEC -Transactor::checkPermission(ReadView const& view, STTx const& tx) +Transactor::checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions) { auto const delegate = tx[~sfDelegate]; if (!delegate) return tesSUCCESS; - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - + auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate)); if (!sle) return terNO_DELEGATE_PERMISSION; - return checkTxPermission(sle, tx); + if (isTesSuccess(checkTxPermission(sle, tx))) + return tesSUCCESS; + + if (!Permission::getInstance().hasGranularPermissions(tx.getTxnType())) + return terNO_DELEGATE_PERMISSION; + + heldGranularPermissions = getGranularPermission(sle, tx.getTxnType()); + if (heldGranularPermissions.empty()) + return terNO_DELEGATE_PERMISSION; + + if (!Permission::getInstance().checkGranularSandbox(tx, heldGranularPermissions)) + return terNO_DELEGATE_PERMISSION; + + return tesSUCCESS; } XRPAmount diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 336bb2004b..caaacfd010 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -181,7 +181,8 @@ invokePreclaim(PreclaimContext const& ctx) if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx)) return result; - if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx)) + if (NotTEC const result = + Transactor::invokeCheckPermission(ctx.view, ctx.tx)) return result; if (NotTEC const result = T::checkSign(ctx)) diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp b/src/libxrpl/tx/transactors/account/AccountSet.cpp index bc207b39dc..36a7e7419f 100644 --- a/src/libxrpl/tx/transactors/account/AccountSet.cpp +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -20,13 +19,11 @@ #include #include #include -#include #include #include #include #include -#include namespace xrpl { @@ -168,54 +165,6 @@ AccountSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -AccountSet::checkPermission(ReadView const& view, STTx const& tx) -{ - // AccountSet is prohibited to be granted on a transaction level, - // but some granular permissions are allowed. - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions); - - auto const uSetFlag = tx.getFieldU32(sfSetFlag); - auto const uClearFlag = tx.getFieldU32(sfClearFlag); - // We don't support any flag based granular permission under - // AccountSet transaction. If any delegated account is trying to - // update the flag on behalf of another account, it is not - // authorized. - if (uSetFlag != 0 || uClearFlag != 0 || ((tx.getFlags() & tfUniversalMask) != 0u)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfEmailHash) && !granularPermissions.contains(AccountEmailHashSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfWalletLocator) || tx.isFieldPresent(sfNFTokenMinter)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfMessageKey) && !granularPermissions.contains(AccountMessageKeySet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfDomain) && !granularPermissions.contains(AccountDomainSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfTransferRate) && !granularPermissions.contains(AccountTransferRateSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfTickSize) && !granularPermissions.contains(AccountTickSizeSet)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER AccountSet::preclaim(PreclaimContext const& ctx) { diff --git a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp index dc6c98f95e..6def542c7d 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp @@ -29,14 +29,12 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx) return terNO_DELEGATE_PERMISSION; } -void -loadGranularPermission( - SLE::const_ref delegate, - TxType const& txType, - std::unordered_set& granularPermissions) +std::unordered_set +getGranularPermission(SLE::const_ref delegate, TxType const& txType) { + std::unordered_set granularPermissions; if (!delegate) - return; + return granularPermissions; auto const permissionArray = delegate->getFieldArray(sfPermissions); for (auto const& permission : permissionArray) @@ -47,6 +45,8 @@ loadGranularPermission( if (type && *type == txType) granularPermissions.insert(granularValue); } + + return granularPermissions; } } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 805ebe3684..9a9a01ec19 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -29,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -273,38 +271,24 @@ Payment::preflight(PreflightContext const& ctx) } NotTEC -Payment::checkPermission(ReadView const& view, STTx const& tx) +Payment::checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) { - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttPAYMENT, granularPermissions); - auto const& dstAmount = tx.getFieldAmount(sfAmount); auto const& amountAsset = dstAmount.asset(); // Granular permissions are only valid for direct payments. - if ((tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) || - tx.isFieldPresent(sfPaths)) + if (tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) return terNO_DELEGATE_PERMISSION; // PaymentMint and PaymentBurn apply to both IOU and MPT direct payments. - if (granularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && + if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && amountAsset.getIssuer() == tx[sfAccount]) return tesSUCCESS; - if (granularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && + if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && amountAsset.getIssuer() == tx[sfDestination]) return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index 9b25531161..683d65b6f9 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -16,14 +15,12 @@ #include #include #include -#include #include #include #include #include #include -#include namespace xrpl { @@ -138,39 +135,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) -{ - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - // this is added in case more flags will be added for MPTokenIssuanceSet - // in the future. Currently unreachable. - if ((tx.getFlags() & tfMPTokenIssuanceSetMask) != 0u) - return terNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); - - if (tx.isFlag(tfMPTLock) && !granularPermissions.contains(MPTokenIssuanceLock)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFlag(tfMPTUnlock) && !granularPermissions.contains(MPTokenIssuanceUnlock)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) { diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 1d2bc96693..7838b212b2 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -21,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -124,51 +122,21 @@ TrustSet::preflight(PreflightContext const& ctx) } NotTEC -TrustSet::checkPermission(ReadView const& view, STTx const& tx) +TrustSet::checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) { - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - // Currently we only support TrustlineAuthorize, TrustlineFreeze and - // TrustlineUnfreeze granular permission. Setting other flags returns - // error. - if ((tx.getFlags() & tfTrustSetPermissionMask) != 0u) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut)) - return terNO_DELEGATE_PERMISSION; - auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount); auto const sleRippleState = view.read( keylet::line( tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get().currency)); - // if the trustline does not exist, granular permissions are - // not allowed to create trustline + // granular permissions are not allowed to create a trustline if (!sleRippleState) return terNO_DELEGATE_PERMISSION; - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttTRUST_SET, granularPermissions); - - if (tx.isFlag(tfSetfAuth) && !granularPermissions.contains(TrustlineAuthorize)) - return terNO_DELEGATE_PERMISSION; - if (tx.isFlag(tfSetFreeze) && !granularPermissions.contains(TrustlineFreeze)) - return terNO_DELEGATE_PERMISSION; - if (tx.isFlag(tfClearFreeze) && !granularPermissions.contains(TrustlineUnfreeze)) - return terNO_DELEGATE_PERMISSION; - - // updating LimitAmount is not allowed only with granular permissions, + // updating LimitAmount is not allowed with granular permissions, // unless there's a new granular permission for this in the future. auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer() ? sleRippleState->getFieldAmount(sfHighLimit) diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 70b091290c..20668a42bf 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -5,8 +5,11 @@ #include #include #include +#include #include +#include #include +#include #include #include #include @@ -22,7 +25,9 @@ #include #include #include +#include +#include #include #include #include @@ -33,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +47,7 @@ #include #include #include +#include #include #include @@ -1063,6 +1070,93 @@ class Delegate_test : public beast::unit_test::Suite } } + // PaymentMint/PaymentBurn with sfSendMax of the same asset is allowed, + // same-asset SendMax is still a direct payment, not cross-currency. + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + + // sfSendMax with same asset as sfAmount, still a direct payment + env(pay(gw, alice, usd(50)), Sendmax(usd(50)), delegate::As(bob)); + env.require(Balance(alice, usd(50))); + + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + env(pay(alice, gw, usd(30)), Sendmax(usd(30)), delegate::As(bob)); + env.require(Balance(alice, usd(20))); + } + + // Test invalid fields or flags not allowed in granular permission template + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + env(delegate::set(gw, bob, {"PaymentMint"})); + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + // sfDeliverMin (with tfPartialPayment) is not in the PaymentMint + // or PaymentBurn template. + env(pay(gw, alice, usd(100)), + DeliverMin(usd(50)), + Txflags(tfPartialPayment), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, usd(50)), + DeliverMin(usd(25)), + Txflags(tfPartialPayment), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + + // sfDomainID is not in the PaymentMint or PaymentBurn template. + env(pay(gw, alice, usd(100)), + Domain(uint256{1}), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, usd(50)), + Domain(uint256{1}), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // Delegate account holds no granular permissions for the tx type: + // getGranularPermission returns empty set. + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + // Bob holds only an AccountSet granular permission. + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + // Payment has granular permissions defined in permissions.macro, + // but bob only holds AccountSet's granular permission, + // getGranularPermission returns empty. + env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + // PaymentMint and PaymentBurn for MPT { std::string logs; @@ -1119,6 +1213,40 @@ class Delegate_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100)); } } + + // Verify granular permissions of different tx types in the same SLE are scoped + // correctly. AccountSet permissions don't apply to Payment and vice versa + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + // Alice granted bob with both AccountDomainSet and PaymentMint. + env(delegate::set(alice, bob, {"AccountDomainSet", "PaymentMint"})); + env.close(); + + // PaymentMint fails at granular semantic check because alice is not the issuer. + env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // AccountDomainSet applies correctly to AccountSet + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // gw gives bob PaymentMint and bob can mint on gw's behalf + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + env(pay(gw, alice, usd(50)), delegate::As(bob)); + env.require(Balance(alice, usd(50))); + } } void @@ -1301,6 +1429,34 @@ class Delegate_test : public beast::unit_test::Suite env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfFullyCanonicalSig), delegate::As(bob)); } + + { + Env env(*this); + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob)); + env.close(); + + // sfQualityOut is a valid TrustSet field, but not permitted in granular template + json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth); + txJson[sfQualityOut.jsonName] = 100; + env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // tfSetNoRipple is a valid flag for TrustSet, but not permitted in granular template + env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfSetNoRipple), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } } void @@ -1456,7 +1612,9 @@ class Delegate_test : public beast::unit_test::Suite env(jv2, Ter(terNO_DELEGATE_PERMISSION)); } - // can not set AccountSet flags on behalf of other account + // can not set AccountSet flags on behalf of other account, + // in permissions.macro, the template for AccountSet does + // not allow any flag set or clear. { Env env(*this); auto const alice = Account{"alice"}; @@ -1552,6 +1710,71 @@ class Delegate_test : public beast::unit_test::Suite env(jt); BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); } + + // setting invalid field not in permissions.macro template will be rejected. + { + Env env(*this); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // Alice gives Bob permission to set her Domain + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + std::string const domain = "example.com"; + auto txJson = noop(alice); + txJson[sfDomain] = strHex(domain); + txJson[sfDelegate] = bob.human(); + + // sfNFTokenMinter is a valid field in AccountSet tx, but + // it is not permitted for granular template + txJson[sfNFTokenMinter] = bob.human(); + + env(txJson, Ter(terNO_DELEGATE_PERMISSION)); + } + + // Delegated AccountSet with no fields and no flags is allowed, + // because it is allowed in the non-delegated case as well. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + auto jt = noop(alice); + jt[sfDelegate] = bob.human(); + env(jt); + } + + // Revoking all permissions deletes the SLE and subsequent attempts are rejected. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + env(jt); + + // empty DelegateSet deletes the SLE + env(delegate::set(alice, bob, {})); + env.close(); + + env(jt, Ter(terNO_DELEGATE_PERMISSION)); + } } void @@ -1672,6 +1895,37 @@ class Delegate_test : public beast::unit_test::Suite env.close(); mpt.set({.account = alice, .flags = tfMPTLock | tfFullyCanonicalSig, .delegate = bob}); } + + // field not permitted to exist in granular delegation + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(100000), alice, bob); + + MPTTester mpt(env, alice, {.fund = false}); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob for MPTokenIssuanceLock + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + + // Field is not permitted, permitted fields for delegation is defined in + // permissions.macro. + mpt.set( + {.account = alice, + .mutableFlags = 2, + .delegate = bob, + .err = terNO_DELEGATE_PERMISSION}); + + // Notice: flags not defined in permissions.macro are not permitted for delegation. + // Since preflight will check invalid flag for the tx, it is not reachable. + // If any new flag is defined into the transaction in the future, + // but is not allowed for delegation, the transaction will be rejected with + // terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in + // permissions.macro. + } } void @@ -2141,6 +2395,62 @@ class Delegate_test : public beast::unit_test::Suite for (auto const& tx : txRequiredFeatures) txAmendmentEnabled(tx.first); } + + // Granular permissions also require the amendment for their underlying + // transaction type. + { + for (auto const permission : {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}) + { + Env env(*this, features - featureMPTokensV1); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {permission}), Ter(temMALFORMED)); + } + } + } + + void + testGranularSandboxCheckOrder() + { + testcase("Make sure GranularSandbox is checked after transaction-level permission"); + + using namespace jtx; + + Env env(*this); + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob)); + env.close(); + + // sfQualityOut is a valid TrustSet field, but not permitted in granular template + json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth); + txJson[sfQualityOut.jsonName] = 100; + env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // Now Alice grants Bob with transaction level permission + env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustSet"})); + env.close(); + + // NOTE: This case is to ensure that if a delegate possesses a + // transaction-level permission (e.g., TrustSet), the granular sandbox must not incorrectly + // block the transaction. The function checkGranularSandbox MUST be called after the + // transaction-level permission check. This test case is to avoid future refactor mistakes, + // modifying the order will fail here. + env(txJson, delegate::As(bob)); } void @@ -2193,6 +2503,94 @@ class Delegate_test : public beast::unit_test::Suite "\n Action: Verify security requirements to interact with Delegation feature"); } + void + testNonDelegableTxWithDelegate(FeatureBitset features) + { + testcase("non-delegable tx with sfDelegate is rejected at preflight"); + using namespace jtx; + + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // Transactions that are notDelegable and have no granular permissions + // will be rejected with temINVALID at preflight. + // Note: pseudo-transactions (EnableAmendment, SetFee and UNLModify) are also + // notDelegable but are excluded here — passesLocalChecks() blocks them + // before preflight1 is ever reached. + { + // SetRegularKey, SignerListSet, AccountDelete, DelegateSet. + env(regkey(alice, bob), delegate::As(bob), Ter(temINVALID)); + env(signers(alice, 1, {{bob, 1}}), delegate::As(bob), Ter(temINVALID)); + env(acctdelete(alice, bob), delegate::As(bob), Ter(temINVALID)); + env(delegate::set(alice, bob, {"Payment"}), delegate::As(bob), Ter(temINVALID)); + + // SAV transactions. + { + Vault const vault{env}; + auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(createTx, delegate::As(bob), Ter(temINVALID)); + + env(vault.set({.owner = alice, .id = keylet.key}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.del({.owner = alice, .id = keylet.key}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(1)}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(1)}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.clawback({.issuer = alice, .id = keylet.key, .holder = bob}), + delegate::As(bob), + Ter(temINVALID)); + } + + // Batch transaction: the outer Batch itself is non-delegable. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 1); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(1)), seq + 1), + delegate::As(bob), + Ter(temINVALID)); + } + + // Lending protocol transactions + { + Vault const vault{env}; + auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(createTx); + + env(loanBroker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loanBroker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loanBroker::coverDeposit(alice, keylet.key, XRP(1)), + delegate::As(bob), + Ter(temINVALID)); + env(loanBroker::coverWithdraw(alice, keylet.key, XRP(1)), + delegate::As(bob), + Ter(temINVALID)); + env(loanBroker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); + + env(loan::set(alice, keylet.key, Number(100)), delegate::As(bob), Ter(temINVALID)); + env(loan::manage(alice, keylet.key, 0), delegate::As(bob), Ter(temINVALID)); + env(loan::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan::pay(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); + } + } + + // AccountSet is notDelegable at tx level but has granular permissions, + // so sfDelegate passes preflight and is rejected at invokeCheckPermission with + // terNO_DELEGATE_PERMISSION. + { + env(fset(alice, asfDefaultRipple), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + } + void testDelegateUtilsNullptrCheck() { @@ -2202,9 +2600,8 @@ class Delegate_test : public beast::unit_test::Suite STTx const tx{ttPAYMENT, [](STObject&) {}}; BEAST_EXPECT(checkTxPermission(nullptr, tx) == terNO_DELEGATE_PERMISSION); - // loadGranularPermission nullptr check - std::unordered_set granularPermissions; - loadGranularPermission(nullptr, ttPAYMENT, granularPermissions); + // getGranularPermission nullptr check + auto const granularPermissions = getGranularPermission(nullptr, ttPAYMENT); BEAST_EXPECT(granularPermissions.empty()); } @@ -2234,7 +2631,9 @@ class Delegate_test : public beast::unit_test::Suite testSignForDelegated(); testPermissionValue(all); testTxRequireFeatures(all); + testGranularSandboxCheckOrder(); testTxDelegableCount(); + testNonDelegableTxWithDelegate(all); testDelegateUtilsNullptrCheck(); } }; From b6a1ad5bb3cea836b4d4571238640c8007ed27d9 Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Thu, 18 Jun 2026 12:21:12 -0700 Subject: [PATCH 034/121] fix: Ensure xrpld service directories exist at startup (#7565) --- package/shared/xrpld.service | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/shared/xrpld.service b/package/shared/xrpld.service index 7f3496acbb..f54e47aa14 100644 --- a/package/shared/xrpld.service +++ b/package/shared/xrpld.service @@ -17,6 +17,10 @@ ProtectHome=true PrivateTmp=true User=xrpld Group=xrpld +StateDirectory=xrpld +StateDirectoryMode=0750 +LogsDirectory=xrpld +LogsDirectoryMode=0750 LimitNOFILE=65536 SystemCallArchitectures=native From b1f794f06792a55bd33e5b544593425713c6cd0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:39:38 -0400 Subject: [PATCH 035/121] ci: [DEPENDABOT] bump actions/checkout from 6.0.3 to 7.0.0 (#7585) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-pr-description.yml | 2 +- .github/workflows/on-pr.yml | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/reusable-check-levelization.yml | 2 +- .github/workflows/reusable-check-rename.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/reusable-package.yml | 6 +++--- .github/workflows/reusable-strategy-matrix.yml | 2 +- .github/workflows/reusable-upload-recipe.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/check-pr-description.yml b/.github/workflows/check-pr-description.yml index a60b83738a..744449f216 100644 --- a/.github/workflows/check-pr-description.yml +++ b/.github/workflows/check-pr-description.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Write PR body to file env: diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 4b2edeb93d..0cc9b375a7 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Determine changed files # This step checks whether any files have changed that should # cause the next jobs to run. We do it this way rather than diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 0de5347aab..cc7b6b6e7e 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -44,7 +44,7 @@ jobs: container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 95e6b0cbe2..3e6464aaba 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -110,7 +110,7 @@ jobs: uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 diff --git a/.github/workflows/reusable-check-levelization.yml b/.github/workflows/reusable-check-levelization.yml index 813c0e1e36..88c95ac3ba 100644 --- a/.github/workflows/reusable-check-levelization.yml +++ b/.github/workflows/reusable-check-levelization.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check levelization run: python .github/scripts/levelization/generate.py - name: Check for differences diff --git a/.github/workflows/reusable-check-rename.yml b/.github/workflows/reusable-check-rename.yml index 5002cc7f40..9a91e98ee3 100644 --- a/.github/workflows/reusable-check-rename.yml +++ b/.github/workflows/reusable-check-rename.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check definitions run: .github/scripts/rename/definitions.sh . - name: Check copyright notices diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 5d2325cb1d..c663f71842 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -45,7 +45,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 0e3f657006..eed4bfc4a3 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -27,7 +27,7 @@ jobs: matrix: ${{ steps.generate.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -45,7 +45,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | .github/actions/generate-version @@ -69,7 +69,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Download pre-built binary uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index 4518a8ffef..c1a1c1a78b 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -23,7 +23,7 @@ jobs: matrix: ${{ steps.generate.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index ba7a0943d9..a389e98771 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -43,7 +43,7 @@ jobs: container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Generate build version number id: version diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 7ca9d13007..5d3712cf9e 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -65,7 +65,7 @@ jobs: uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 From e29b523620ee47cc1ecd1df523b80ffdcacc1c9e Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 22 Jun 2026 18:00:40 +0100 Subject: [PATCH 036/121] ci: Build and push docker images in forks too (#7588) --- .github/workflows/build-nix-images.yml | 6 +++--- .github/workflows/build-packaging-images.yml | 6 +++--- .github/workflows/pre-commit.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-nix-images.yml b/.github/workflows/build-nix-images.yml index 3af6a3b1d4..54911ef6e0 100644 --- a/.github/workflows/build-nix-images.yml +++ b/.github/workflows/build-nix-images.yml @@ -54,9 +54,9 @@ jobs: base_image: debian:bookworm - name: rhel base_image: registry.access.redhat.com/ubi9/ubi:latest - uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07 + uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c with: - image_name: ghcr.io/xrplf/xrpld/nix-${{ matrix.distro.name }} + image_name: xrpld/nix-${{ matrix.distro.name }} dockerfile: nix/docker/Dockerfile base_image: ${{ matrix.distro.base_image }} - push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }} + push: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/build-packaging-images.yml b/.github/workflows/build-packaging-images.yml index d6dabb0f95..3633847ef3 100644 --- a/.github/workflows/build-packaging-images.yml +++ b/.github/workflows/build-packaging-images.yml @@ -38,9 +38,9 @@ jobs: base_image: debian:bookworm - name: rhel base_image: registry.access.redhat.com/ubi9/ubi:latest - uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07 + uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c with: - image_name: ghcr.io/xrplf/xrpld/packaging-${{ matrix.distro.name }} + image_name: xrpld/packaging-${{ matrix.distro.name }} dockerfile: package/Dockerfile base_image: ${{ matrix.distro.base_image }} - push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }} + push: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index aecf0c2a8b..0363534af5 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@312aaab296060ff89d7f798dcab59f019bea6e02 + uses: XRPLF/actions/.github/workflows/pre-commit.yml@e06d4138c9ec8dceeb7c818645faa38087ea9e3d with: runs_on: ubuntu-latest container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index c663f71842..e99ef574bf 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -32,7 +32,7 @@ jobs: if: ${{ inputs.check_only_changed }} permissions: contents: read - uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@312aaab296060ff89d7f798dcab59f019bea6e02 + uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@c7045074aafe9fb92fa537aa4446f81fbfc17e8b run-clang-tidy: name: Run clang tidy From 997267f84555ca9758394681e26b888eb0699bed Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Mon, 22 Jun 2026 13:36:06 -0400 Subject: [PATCH 037/121] feat: Remove clear mutable flags for DynamicMPT XLS-94 (#7439) --- include/xrpl/protocol/LedgerFormats.h | 12 +- include/xrpl/protocol/TxFlags.h | 42 +- include/xrpl/protocol/detail/features.macro | 2 +- .../transactors/token/MPTokenIssuanceSet.cpp | 66 +- src/test/app/AMMMPT_test.cpp | 16 +- src/test/app/Loan_test.cpp | 189 +-- src/test/app/MPToken_test.cpp | 1260 ++++++++--------- src/test/app/Vault_test.cpp | 243 +--- src/test/jtx/impl/mpt.cpp | 72 +- 9 files changed, 684 insertions(+), 1218 deletions(-) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 99d5d818f1..c1274e9e91 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -180,12 +180,12 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfMPTCanClawback, 0x00000040)) \ \ LEDGER_OBJECT(MPTokenIssuanceMutable, \ - LSF_FLAG(lsmfMPTCanMutateCanLock, 0x00000002) \ - LSF_FLAG(lsmfMPTCanMutateRequireAuth, 0x00000004) \ - LSF_FLAG(lsmfMPTCanMutateCanEscrow, 0x00000008) \ - LSF_FLAG(lsmfMPTCanMutateCanTrade, 0x00000010) \ - LSF_FLAG(lsmfMPTCanMutateCanTransfer, 0x00000020) \ - LSF_FLAG(lsmfMPTCanMutateCanClawback, 0x00000040) \ + LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \ + LSF_FLAG(lsmfMPTCanEnableRequireAuth, 0x00000004) \ + LSF_FLAG(lsmfMPTCanEnableCanEscrow, 0x00000008) \ + LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \ + LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \ + LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \ LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \ LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \ \ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 4652cc1bf0..f9c7bc1a5d 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -341,38 +341,32 @@ inline constexpr FlagValue tfTrustSetPermissionMask = // MPTokenIssuanceCreate MutableFlags: // Indicating specific fields or flags may be changed after issuance. -inline constexpr FlagValue tmfMPTCanMutateCanLock = lsmfMPTCanMutateCanLock; -inline constexpr FlagValue tmfMPTCanMutateRequireAuth = lsmfMPTCanMutateRequireAuth; -inline constexpr FlagValue tmfMPTCanMutateCanEscrow = lsmfMPTCanMutateCanEscrow; -inline constexpr FlagValue tmfMPTCanMutateCanTrade = lsmfMPTCanMutateCanTrade; -inline constexpr FlagValue tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTransfer; -inline constexpr FlagValue tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback; +inline constexpr FlagValue tmfMPTCanEnableCanLock = lsmfMPTCanEnableCanLock; +inline constexpr FlagValue tmfMPTCanEnableRequireAuth = lsmfMPTCanEnableRequireAuth; +inline constexpr FlagValue tmfMPTCanEnableCanEscrow = lsmfMPTCanEnableCanEscrow; +inline constexpr FlagValue tmfMPTCanEnableCanTrade = lsmfMPTCanEnableCanTrade; +inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTransfer; +inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback; inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata; inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee; inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask = - ~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow | - tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | + ~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow | + tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee); // MPTokenIssuanceSet MutableFlags: -// Set or Clear flags. +// Enable mutable capability flags. These flags are one-way: once enabled, +// the corresponding capability cannot be disabled by MPTokenIssuanceSet. inline constexpr FlagValue tmfMPTSetCanLock = 0x00000001; -inline constexpr FlagValue tmfMPTClearCanLock = 0x00000002; -inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000004; -inline constexpr FlagValue tmfMPTClearRequireAuth = 0x00000008; -inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000010; -inline constexpr FlagValue tmfMPTClearCanEscrow = 0x00000020; -inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000040; -inline constexpr FlagValue tmfMPTClearCanTrade = 0x00000080; -inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000100; -inline constexpr FlagValue tmfMPTClearCanTransfer = 0x00000200; -inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000400; -inline constexpr FlagValue tmfMPTClearCanClawback = 0x00000800; -inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~( - tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | - tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade | - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | tmfMPTClearCanClawback); +inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000002; +inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004; +inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008; +inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010; +inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020; +inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = + ~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade | + tmfMPTSetCanTransfer | tmfMPTSetCanClawback); // 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 d25b0b1f2c..573dca951d 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -24,7 +24,7 @@ XRPL_FEATURE(LendingProtocol, Supported::Yes, VoteBehavior::DefaultN XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (IncludeKeyletFields, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(DynamicMPT, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(DynamicMPT, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (TokenEscrowV1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (PriceOracleOrder, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index 683d65b6f9..1cb12709d5 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -38,35 +38,34 @@ MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) return tfMPTokenIssuanceSetMask; } -// Maps set/clear mutable flags in an MPTokenIssuanceSet transaction to the -// corresponding ledger mutable flags that control whether the change is -// allowed. +// Maps each MPTokenIssuanceSet MutableFlags to the corresponding mutable +// flag and the target ledger flag to mutate. struct MPTMutabilityFlags { std::uint32_t setFlag; - std::uint32_t clearFlag; - std::uint32_t canMutateFlag; + std::uint32_t canEnableFlag; + std::uint32_t ledgerFlag; }; static constexpr std::array kMptMutabilityFlags = { {{.setFlag = tmfMPTSetCanLock, - .clearFlag = tmfMPTClearCanLock, - .canMutateFlag = lsmfMPTCanMutateCanLock}, + .canEnableFlag = lsmfMPTCanEnableCanLock, + .ledgerFlag = lsfMPTCanLock}, {.setFlag = tmfMPTSetRequireAuth, - .clearFlag = tmfMPTClearRequireAuth, - .canMutateFlag = lsmfMPTCanMutateRequireAuth}, + .canEnableFlag = lsmfMPTCanEnableRequireAuth, + .ledgerFlag = lsfMPTRequireAuth}, {.setFlag = tmfMPTSetCanEscrow, - .clearFlag = tmfMPTClearCanEscrow, - .canMutateFlag = lsmfMPTCanMutateCanEscrow}, + .canEnableFlag = lsmfMPTCanEnableCanEscrow, + .ledgerFlag = lsfMPTCanEscrow}, {.setFlag = tmfMPTSetCanTrade, - .clearFlag = tmfMPTClearCanTrade, - .canMutateFlag = lsmfMPTCanMutateCanTrade}, + .canEnableFlag = lsmfMPTCanEnableCanTrade, + .ledgerFlag = lsfMPTCanTrade}, {.setFlag = tmfMPTSetCanTransfer, - .clearFlag = tmfMPTClearCanTransfer, - .canMutateFlag = lsmfMPTCanMutateCanTransfer}, + .canEnableFlag = lsmfMPTCanEnableCanTransfer, + .ledgerFlag = lsfMPTCanTransfer}, {.setFlag = tmfMPTSetCanClawback, - .clearFlag = tmfMPTClearCanClawback, - .canMutateFlag = lsmfMPTCanMutateCanClawback}}}; + .canEnableFlag = lsmfMPTCanEnableCanClawback, + .ledgerFlag = lsfMPTCanClawback}}}; NotTEC MPTokenIssuanceSet::preflight(PreflightContext const& ctx) @@ -118,17 +117,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { if ((*mutableFlags == 0u) || ((*mutableFlags & tmfMPTokenIssuanceSetMutableMask) != 0u)) return temINVALID_FLAG; - - // Can not set and clear the same flag - if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags](auto const& f) { - return (*mutableFlags & f.setFlag) && (*mutableFlags & f.clearFlag); - })) - return temINVALID_FLAG; - - // Trying to set a non-zero TransferFee and clear MPTCanTransfer - // in the same transaction is not allowed. - if ((transferFee.value_or(0) != 0u) && ((*mutableFlags & tmfMPTClearCanTransfer) != 0u)) - return temMALFORMED; } } @@ -196,16 +184,9 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if (auto const mutableFlags = ctx.tx[~sfMutableFlags]) { if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags, &isMutableFlag](auto const& f) { - return !isMutableFlag(f.canMutateFlag) && - ((*mutableFlags & (f.setFlag | f.clearFlag))); + return !isMutableFlag(f.canEnableFlag) && ((*mutableFlags & f.setFlag) != 0u); })) return tecNO_PERMISSION; - - // Clearing lsfMPTRequireAuth is invalid when the issuance already has - // a DomainID set, because a DomainID requires RequireAuth to be active. - if ((*mutableFlags & tmfMPTClearRequireAuth) != 0u && - sleMptIssuance->isFieldPresent(sfDomainID)) - return tecNO_PERMISSION; } if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata)) @@ -265,19 +246,8 @@ MPTokenIssuanceSet::doApply() { if ((mutableFlags & f.setFlag) != 0u) { - flagsOut |= f.canMutateFlag; + flagsOut |= f.ledgerFlag; } - else if ((mutableFlags & f.clearFlag) != 0u) - { - flagsOut &= ~f.canMutateFlag; - } - } - - if ((mutableFlags & tmfMPTClearCanTransfer) != 0u) - { - // If the lsfMPTCanTransfer flag is being cleared, then also clear - // the TransferFee field. - sle->makeFieldAbsent(sfTransferFee); } } diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 31b54ceee0..5b576b41e4 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -2240,7 +2240,9 @@ private: .err = Ter(tecNO_AUTH)}); } - // MPTCanTransfer is not set and the account is not the issuer of MPT + // MPTCanTransfer is not set and the account is not the issuer of MPT. + // The issuer can create the AMM, and an existing LP token holder can + // still withdraw. { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); @@ -2250,15 +2252,17 @@ private: .issuer = gw_, .holders = {alice_}, .pay = 30'000, - .flags = kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer, + .flags = tfMPTCanTrade, .authHolder = true}); AMM amm(env, gw_, XRP(10'000), btc(10'000)); - amm.deposit(DepositArg{.account = alice_, .asset1In = XRP(200), .asset2In = btc(200)}); - // Allow to withdraw if transfer is disabled - btc.set({.mutableFlags = tmfMPTClearCanTransfer}); + auto const lpIssue = amm.lptIssue(); + env.trust(STAmount{lpIssue, 20'000'000}, alice_); + env.close(); + env(pay(gw_, alice_, LPToken(1'000'000).tokens(lpIssue))); + env.close(); + amm.withdraw( WithdrawArg{.account = alice_, .asset1Out = btc(100), .assets = {{XRP, btc}}}); } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c3b5850231..dbb0033368 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -5423,110 +5422,12 @@ protected: } void - testCoverDepositWithdrawNonTransferableMPT(FeatureBitset feature) + testLendingCanTradeDisabledNoImpact() { - testcase("CoverDeposit blocked, CoverWithdraw allowed when CanTransfer cleared"); - using namespace jtx; - using namespace loanBroker; - - Env env(*this, feature); - - Account const issuer{"issuer"}; - Account const alice{"alice"}; - - env.fund(XRP(100'000), issuer, alice); - env.close(); - - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {alice}, - .pay = 100, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mpt["MPT"]; - - Vault const vault{env}; - auto const [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); - env(createTx); - env.close(); - - auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, vaultKeylet.key)); - env.close(); - - auto const brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - - Account const pseudoAccount{"Loan Broker pseudo-account", brokerSle->at(sfAccount)}; - - // First, deposit some cover while CanTransfer is set so we have an - // existing position to withdraw from after the governance action. - auto const depositAmount = asset(1); - env(coverDeposit(alice, brokerKeylet.key, depositAmount)); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(Balance(pseudoAccount, depositAmount)); - } - - // Issuer governance: clear CanTransfer. - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Standard Payment path still forbids third-party transfers. - auto const err = feature[featureMPTokensV2] ? tecNO_PERMISSION : tecNO_AUTH; - env(pay(alice, pseudoAccount, asset(1)), Ter(err)); - env.close(); - - // New cover deposits are blocked - this would create new exposure. - env(coverDeposit(alice, brokerKeylet.key, depositAmount), Ter{tecNO_AUTH}); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(Balance(pseudoAccount, depositAmount)); - } - - bool const postAmendment = feature[fixCleanup3_2_0]; - if (postAmendment) - { - // Post-fixCleanup3_2_0: existing cover can always be withdrawn - // even when CanTransfer is cleared, so the broker is not trapped. - env(coverWithdraw(alice, brokerKeylet.key, depositAmount)); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); - env.require(Balance(pseudoAccount, asset(0))); - } - } - else - { - // Pre-fixCleanup3_2_0 regression: cover withdraw was blocked, - // trapping the broker's first-loss capital. - env(coverWithdraw(alice, brokerKeylet.key, depositAmount), Ter{tecNO_AUTH}); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(Balance(pseudoAccount, depositAmount)); - } - } - } - - void - testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared() - { - testcase("LoanSet blocked, LoanPay allowed when CanTransfer cleared"); + testcase("Lending: CanTrade disabled has no impact"); using namespace jtx; using namespace loan; + using namespace loanBroker; Env env(*this, all_); @@ -5542,67 +5443,7 @@ protected: .issuer = issuer, .holders = {lender, borrower}, .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mpt.issuanceID(); - env(pay(issuer, lender, asset(10'000'000))); - // Fund the borrower with enough to cover principal+interest+fees - env(pay(issuer, borrower, asset(100'000))); - env.close(); - - // Create vault and broker while CanTransfer is set. - auto const broker = createVaultAndBroker(env, asset, lender); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - // Create an existing loan while CanTransfer is set. - env(set(borrower, broker.brokerID, 1'000), - Sig(sfCounterpartySignature, lender), - loanSetFee); - env.close(); - auto const loanKeylet = keylet::loan(broker.brokerID, 1); - BEAST_EXPECT(env.le(loanKeylet)); - - // Issuer governance: clear CanTransfer. - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Issuing a NEW loan is blocked - it would create new exposure into - // a pool the issuer is restricting. - env(set(borrower, broker.brokerID, 1'000), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter{tecNO_AUTH}); - env.close(); - - // Repaying an existing loan is always allowed - blocking it would - // create irrecoverable bad debt and trap SAV depositor principal. - env(pay(borrower, loanKeylet.key, asset(1'000))); - env.close(); - } - - void - testLendingCanTradeClearedNoImpact() - { - testcase("Lending: CanTrade cleared has no impact"); - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); PrettyAsset const asset = mpt.issuanceID(); env(pay(issuer, lender, asset(10'000'000))); env(pay(issuer, borrower, asset(100'000))); @@ -5610,16 +5451,7 @@ protected: auto const broker = createVaultAndBroker(env, asset, lender); - // Sanity: while CanTrade is set, the asset can be placed on the DEX. - env(offer(lender, XRP(1), asset(10))); - env.close(); - - // Issuer governance: clear CanTrade. Loan origination and repayment - // are not trades: nothing in the Lending Protocol should be impacted. - mpt.set({.mutableFlags = tmfMPTClearCanTrade}); - env.close(); - - // Control: clearing CanTrade is observable on the DEX path. + // CanTrade is not set env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); env.close(); @@ -5644,6 +5476,13 @@ protected: // Cover withdrawal still works. env(coverWithdraw(lender, broker.brokerID, asset(100))); env.close(); + + // Enable CanTrade and verify the DEX path is restored. + mpt.set({.mutableFlags = tmfMPTSetCanTrade}); + env.close(); + + env(offer(lender, XRP(1), asset(10))); + env.close(); } #if LOAN_TODO @@ -8716,8 +8555,7 @@ protected: testRIPD3901(); testBorrowerIsBroker(); testLimitExceeded(); - testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared(); - testLendingCanTradeClearedNoImpact(); + testLendingCanTradeDisabledNoImpact(); testBugOverpaymentPrincipalChange(); testBugOverpayUnroundedAmount(); @@ -8747,7 +8585,6 @@ protected: testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features); testBatchBypassCounterparty(features); testLoanNextPaymentDueDateOverflow(features); - testCoverDepositWithdrawNonTransferableMPT(features); testSequentialFLCDepletion(features); // Invariants diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 3d6cff0885..2cab3e7c89 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -3485,7 +3485,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanMutateCanLock | + .mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanEnableCanLock | tmfMPTCanMutateTransferFee}); // Setting flags is not allowed when MutableFlags is present @@ -3533,33 +3533,6 @@ class MPToken_test : public beast::unit_test::Suite } } - // Can not set and clear the same mutable flag - { - Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {bob}}); - auto const mptID = makeMptID(env.seq(alice), alice); - - auto const flagCombinations = { - tmfMPTSetCanLock | tmfMPTClearCanLock, - tmfMPTSetRequireAuth | tmfMPTClearRequireAuth, - tmfMPTSetCanEscrow | tmfMPTClearCanEscrow, - tmfMPTSetCanTrade | tmfMPTClearCanTrade, - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer, - tmfMPTSetCanClawback | tmfMPTClearCanClawback, - tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade, - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanEscrow | - tmfMPTClearCanClawback}; - - for (auto const& mutableFlags : flagCombinations) - { - mptAlice.set( - {.account = alice, - .id = mptID, - .mutableFlags = mutableFlags, - .err = temINVALID_FLAG}); - } - } - // Can not mutate flag which is not mutable { Env env{*this, features}; @@ -3569,17 +3542,11 @@ class MPToken_test : public beast::unit_test::Suite auto const mutableFlags = { tmfMPTSetCanLock, - tmfMPTClearCanLock, tmfMPTSetRequireAuth, - tmfMPTClearRequireAuth, tmfMPTSetCanEscrow, - tmfMPTClearCanEscrow, tmfMPTSetCanTrade, - tmfMPTClearCanTrade, tmfMPTSetCanTransfer, - tmfMPTClearCanTransfer, - tmfMPTSetCanClawback, - tmfMPTClearCanClawback}; + tmfMPTSetCanClawback}; for (auto const& mutableFlag : mutableFlags) { @@ -3623,34 +3590,6 @@ class MPToken_test : public beast::unit_test::Suite .err = temBAD_TRANSFER_FEE}); } - // Test setting non-zero transfer fee and clearing MPTCanTransfer at the - // same time - { - Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {bob}}); - - mptAlice.create( - {.transferFee = 100, - .ownerCount = 1, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); - - // Can not set non-zero transfer fee and clear MPTCanTransfer at the - // same time - mptAlice.set( - {.account = alice, - .mutableFlags = tmfMPTClearCanTransfer, - .transferFee = 1, - .err = temMALFORMED}); - - // Can set transfer fee to zero and clear MPTCanTransfer at the same - // time. tfMPTCanTransfer will be cleared and TransferFee field will - // be removed. - mptAlice.set( - {.account = alice, .mutableFlags = tmfMPTClearCanTransfer, .transferFee = 0}); - BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - } - // Can not set non-zero transfer fee when MPTCanTransfer is not set { Env env{*this, features}; @@ -3658,7 +3597,7 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanEnableCanTransfer}); mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); @@ -3691,21 +3630,14 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | + .mutableFlags = tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanMutateMetadata}); // Can not mutate transfer fee mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); auto const invalidFlags = { - tmfMPTSetCanLock, - tmfMPTClearCanLock, - tmfMPTSetRequireAuth, - tmfMPTClearRequireAuth, - tmfMPTSetCanEscrow, - tmfMPTClearCanEscrow, - tmfMPTSetCanClawback, - tmfMPTClearCanClawback}; + tmfMPTSetCanLock, tmfMPTSetRequireAuth, tmfMPTSetCanEscrow, tmfMPTSetCanClawback}; // Can not mutate flags which are not mutable for (auto const& mutableFlag : invalidFlags) @@ -3716,11 +3648,9 @@ class MPToken_test : public beast::unit_test::Suite // Can mutate MPTCanTrade mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade}); // Can mutate MPTCanTransfer mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); // Can mutate metadata mptAlice.set({.account = alice, .metadata = "test"}); @@ -3789,37 +3719,26 @@ class MPToken_test : public beast::unit_test::Suite BEAST_EXPECT(mptAlice.checkTransferFee(10)); } - // Test flag toggling + // Test mutable flag enablement { - auto testFlagToggle = [&](std::uint32_t createFlags, - std::uint32_t setFlags, - std::uint32_t clearFlags) { + auto testFlagSet = [&](std::uint32_t createFlags, std::uint32_t setFlags) { Env env{*this, features}; MPTTester mptAlice(env, alice); // Create the MPT object with the specified initial flags mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = createFlags}); - // Set and clear the flag multiple times - mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); + // Setting the same mutable capability more than once is harmless. mptAlice.set({.account = alice, .mutableFlags = setFlags}); mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); - mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); }; - testFlagToggle(tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock); - testFlagToggle( - tmfMPTCanMutateRequireAuth, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth); - testFlagToggle(tmfMPTCanMutateCanEscrow, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow); - testFlagToggle(tmfMPTCanMutateCanTrade, tmfMPTSetCanTrade, tmfMPTClearCanTrade); - testFlagToggle( - tmfMPTCanMutateCanTransfer, tmfMPTSetCanTransfer, tmfMPTClearCanTransfer); - testFlagToggle( - tmfMPTCanMutateCanClawback, tmfMPTSetCanClawback, tmfMPTClearCanClawback); + testFlagSet(tmfMPTCanEnableCanLock, tmfMPTSetCanLock); + testFlagSet(tmfMPTCanEnableRequireAuth, tmfMPTSetRequireAuth); + testFlagSet(tmfMPTCanEnableCanEscrow, tmfMPTSetCanEscrow); + testFlagSet(tmfMPTCanEnableCanTrade, tmfMPTSetCanTrade); + testFlagSet(tmfMPTCanEnableCanTransfer, tmfMPTSetCanTransfer); + testFlagSet(tmfMPTCanEnableCanClawback, tmfMPTSetCanClawback); } } @@ -3840,7 +3759,7 @@ class MPToken_test : public beast::unit_test::Suite {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanTrade | + .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanTrade | tmfMPTCanMutateTransferFee}); mptAlice.authorize({.account = bob, .holderCount = 1}); @@ -3848,11 +3767,8 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); // Can mutate the mutable flags and fields - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade}); mptAlice.set({.account = alice, .transferFee = 200}); } @@ -3864,7 +3780,7 @@ class MPToken_test : public beast::unit_test::Suite {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | + .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanClawback | tmfMPTCanMutateMetadata}); mptAlice.authorize({.account = bob, .holderCount = 1}); @@ -3872,36 +3788,23 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set({.account = alice, .flags = tfMPTLock}); // Can mutate the mutable flags and fields - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback}); mptAlice.set({.account = alice, .metadata = "mutate"}); } - // Test lock and unlock after mutating MPTCanLock + // Test lock and unlock after enabling MPTCanLock { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, - .flags = tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | + .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanClawback | tmfMPTCanMutateMetadata}); mptAlice.authorize({.account = bob, .holderCount = 1}); - // Can lock and unlock - mptAlice.set({.account = alice, .flags = tfMPTLock}); - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); - mptAlice.set({.account = alice, .flags = tfMPTUnlock}); - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); - - // Clear lsfMPTCanLock - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); - - // Can not lock or unlock + // Can not lock or unlock before MPTCanLock is enabled mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION}); mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); mptAlice.set( @@ -3909,10 +3812,10 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set( {.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); - // Set MPTCanLock again + // Set MPTCanLock mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - // Can lock and unlock again + // Can lock and unlock mptAlice.set({.account = alice, .flags = tfMPTLock}); mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); mptAlice.set({.account = alice, .flags = tfMPTUnlock}); @@ -3926,83 +3829,30 @@ class MPToken_test : public beast::unit_test::Suite testcase("Mutate MPTRequireAuth"); using namespace test::jtx; - // test mutating RequireAuth flag on the issuance and its effect on payment authorization - { - Env env{*this, features}; - Account const alice("alice"); - Account const bob("bob"); + // test enabling RequireAuth flag on the issuance and its effect on payment + // authorization + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); - MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .flags = tfMPTRequireAuth, - .mutableFlags = tmfMPTCanMutateRequireAuth}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = tmfMPTCanEnableRequireAuth}); - mptAlice.authorize({.account = bob}); - mptAlice.authorize({.account = alice, .holder = bob}); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 1000); - // Pay to bob - mptAlice.pay(alice, bob, 1000); + // Set RequireAuth because it is mutable. + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); - // Unauthorize bob - mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); + // This should fail because bob is not authorized yet. + mptAlice.pay(alice, bob, 1000, tecNO_AUTH); - // Can not pay to bob - mptAlice.pay(bob, alice, 100, tecNO_AUTH); - - // Clear RequireAuth - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth}); - - // Can pay to bob - mptAlice.pay(alice, bob, 1000); - - // Set RequireAuth again - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); - - // Can not pay to bob since he is not authorized - mptAlice.pay(bob, alice, 100, tecNO_AUTH); - - // Authorize bob again - mptAlice.authorize({.account = alice, .holder = bob}); - - // Can pay to bob again - mptAlice.pay(alice, bob, 100); - } - - // Cannot clear RequireAuth when a DomainID is set on the issuance - { - Account const alice{"alice"}; - Account const bob{"bob"}; - Account const credIssuer{"credIssuer"}; - pdomain::Credentials const credentials{ - {.issuer = credIssuer, .credType = "credential"}}; - - Env env{*this, features}; - env.fund(XRP(1000), credIssuer); - env.close(); - - env(pdomain::setTx(credIssuer, credentials)); - env.close(); - auto const domainId = pdomain::getNewDomain(env.meta()); - - MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create({ - .ownerCount = 1, - .flags = tfMPTRequireAuth, - .mutableFlags = tmfMPTCanMutateRequireAuth, - .domainID = domainId, - }); - - // Clearing RequireAuth while a DomainID is present must be rejected, - mptAlice.set({ - .account = alice, - .mutableFlags = tmfMPTClearRequireAuth, - .err = tecNO_PERMISSION, - }); - - // Setting RequireAuth (already set) is still allowed, though it has no effect. - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); - } + // Issuer authorizes bob and pay should succeed. + mptAlice.authorize({.account = alice, .holder = bob}); + mptAlice.pay(alice, bob, 1000); } void @@ -4023,7 +3873,7 @@ class MPToken_test : public beast::unit_test::Suite {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanEscrow}); + .mutableFlags = tmfMPTCanEnableCanEscrow}); mptAlice.authorize({.account = carol}); mptAlice.authorize({.account = bob}); @@ -4045,14 +3895,6 @@ class MPToken_test : public beast::unit_test::Suite escrow::kCondition(escrow::kCb1), escrow::kFinishTime(env.now() + 1s), Fee(baseFee * 150)); - - // Clear MPTCanEscrow - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanEscrow}); - env(escrow::create(carol, bob, mpt(3)), - escrow::kCondition(escrow::kCb1), - escrow::kFinishTime(env.now() + 1s), - Fee(baseFee * 150), - Ter(tecNO_PERMISSION)); } void @@ -4071,7 +3913,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee}); + .mutableFlags = tmfMPTCanEnableCanTransfer | tmfMPTCanMutateTransferFee}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); @@ -4115,19 +3957,9 @@ class MPToken_test : public beast::unit_test::Suite env(pay(bob, carol, mptAlice(50)), Txflags(tfPartialPayment)); BEAST_EXPECT(env.balance(carol, mptc) == mptc(49)); } - - // Alice clears MPTCanTransfer - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); - - // TransferFee field is removed when MPTCanTransfer is cleared - BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - - // Bob can not pay - mptAlice.pay(bob, carol, 50, tecNO_AUTH); } - // Can set transfer fee to zero when MPTCanTransfer is not set, but - // tmfMPTCanMutateTransferFee is set. + // Can set transfer fee to zero when tmfMPTCanMutateTransferFee is set. { Env env{*this, features}; @@ -4136,18 +3968,12 @@ class MPToken_test : public beast::unit_test::Suite {.transferFee = 100, .ownerCount = 1, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanMutateTransferFee}); BEAST_EXPECT(mptAlice.checkTransferFee(100)); - // Clear MPTCanTransfer and transfer fee is removed - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); - BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - - // Can still set transfer fee to zero, although it is already zero + // Setting transfer fee to zero removes the field. mptAlice.set({.account = alice, .transferFee = 0}); - - // TransferFee field is still not present BEAST_EXPECT(!mptAlice.isTransferFeePresent()); } } @@ -4165,7 +3991,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( - {.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanMutateCanClawback}); + {.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanEnableCanClawback}); // Bob creates an MPToken mptAlice.authorize({.account = bob}); @@ -4181,12 +4007,6 @@ class MPToken_test : public beast::unit_test::Suite // Can clawback now mptAlice.claw(alice, bob, 1); - - // Clear MPTCanClawback - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback}); - - // Can not clawback - mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); } void @@ -4536,27 +4356,22 @@ class MPToken_test : public beast::unit_test::Suite { Env env(*this); env.fund(XRP(1'000), gw, alice, carol); - MPTTester btc( + MPTTester const btc( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, - .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester eth( + .flags = tfMPTCanTrade | tfMPTCanTransfer}); + MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, - .flags = tfMPTCanTrade | tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .flags = tfMPTCanTrade}); // Can create env(offer(alice, eth(10), btc(10)), Txflags(tfPassive)); - btc.set({.mutableFlags = tmfMPTSetCanTransfer}); - eth.set({.mutableFlags = tmfMPTClearCanTransfer}); - env(offer(alice, eth(10), btc(10)), Txflags(tfPassive)); - BEAST_EXPECT(getAccountOffers(env, alice)[jss::offers].size() == 2); + BEAST_EXPECT(getAccountOffers(env, alice)[jss::offers].size() == 1); // issuer can create env(offer(gw, eth(10), btc(10)), Txflags(tfPassive)); @@ -4584,14 +4399,14 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol}, .pay = 100, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); // Can't create env(offer(gw, eth(10), btc(10)), Ter(tecNO_PERMISSION)); @@ -4828,29 +4643,29 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol, bob}, .pay = 1'000, .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanTrade | - tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanTrade | + tmfMPTCanEnableCanTransfer}); MPTTester eth( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); MPTTester const usd( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); MPTTester const cad( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive)); env.close(); @@ -4896,13 +4711,33 @@ class MPToken_test : public beast::unit_test::Suite // BTC is transferred from ed to bob, ed is not authorized env(pay(ed, gw, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecNO_AUTH)); env.close(); - btc.set({.mutableFlags = tmfMPTClearRequireAuth}); + } - // MPTCanTransfer is not set + // MPTCanTransfer is not set. + { + auto const ed = Account{"ed"}; + Env env{*this, features}; + env.fund(XRP(1'000), gw, alice, carol, bob, ed); + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob, ed}, + .pay = 1'000, + .flags = tfMPTCanTrade}); + MPTTester const eth( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob, ed}, + .pay = 1'000, + .flags = kMptDexFlags}); + + env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive)); + env.close(); + env(offer(bob, btc(1'000), eth(1'000)), Txflags(tfPassive)); + env.close(); // Fail regardless if source/destination is the issuer or // not since the offer is owned by a holder. - btc.set({.mutableFlags = tmfMPTClearCanTransfer}); env(pay(ed, carol, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL)); env(pay(carol, ed, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL)); env(pay(ed, carol, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecPATH_PARTIAL)); @@ -4926,124 +4761,166 @@ class MPToken_test : public beast::unit_test::Suite env(pay(ed, gw, btc(10)), Path(~btc), Sendmax(eth(10))); env.close(); } - // Multiple steps: CAD/USD, USD/BTC, BTC/ETH + + // Multiple steps: CAD/USD, USD/BTC, BTC/ETH. + // takerGets can transfer if: + // - CanTransfer is set + // - The offer's owner is the issuer + // - BookStep is the last step, which means strand's destination is + // the issuer + // takerPays can transfer if + // - BookStep is the first step, which means strand's source is + // the issuer + // - The offer's owner is the issuer + // - Previous step is BookStep, which transfers per above + // - CanTransfer is set { - auto const ed = Account{"ed"}; - Env env{*this, features}; - env.fund(XRP(1'000), gw, alice, carol, bob, ed); - env.close(); - MPTTester btc( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester eth( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester usd( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester cad( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - // takerGets can transfer if: - // - CanTransfer is set - // - The offer's owner is the issuer - // - BookStep is the last step, which means strand's destination is - // the issuer - // takerPays can transfer if - // - BookStep is the first step, which means strand's source is - // the issuer - // - The offer's owner is the issuer - // - Previous step is BookStep, which transfers per above - // - CanTransfer is set - env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); - env(offer(bob, usd(100), btc(100)), Txflags(tfPassive)); - env(offer(bob, btc(100), eth(100)), Txflags(tfPassive)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - btc.set({.mutableFlags = tmfMPTSetCanTransfer}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // TakerGets - // fail - CAD/USD is owned by bob - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - auto seq(env.seq(gw)); - env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); - env.close(); - // fail - CAD/USD is owned by bob - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - env.close(); - env(offerCancel(gw, seq)); - env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - // succeed - CAD/USD is owned by issuer - env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - // bob's CAD/USD is deleted - BEAST_EXPECT(expectOffers(env, bob, 2)); - env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); - BEAST_EXPECT(expectOffers(env, gw, 0)); - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - eth.set({.mutableFlags = tmfMPTClearCanTransfer}); - // fail - BTC/ETH is owned by bob, destination is carol - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - // succeed - destination is an issuer - env(pay(alice, gw, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - // TakerPays - eth.set({.mutableFlags = tmfMPTSetCanTransfer}); - cad.set({.mutableFlags = tmfMPTClearCanTransfer}); - // fail - CAD/USD is owned by bob, source is alice - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - // succeed - source is the issuer - env(pay(gw, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); - env.close(); - // succeed - CAD/USD is owned by issuer - env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - BEAST_EXPECT(expectOffers(env, gw, 0)); - BEAST_EXPECT(expectOffers(env, bob, 2)); - cad.set({.mutableFlags = tmfMPTSetCanTransfer}); - btc.set({.mutableFlags = tmfMPTClearCanTransfer}); - env(offer(bob, cad(1), usd(1)), Txflags(tfPassive)); - env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); - env.close(); - // succeed - USD/BTC is owned by issuer - env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - BEAST_EXPECT(expectOffers(env, gw, 0)); + // enum to indicate which MPT doesn't set CanTransfer flag. + enum class NoTransferMPT { BTC, ETH, USD, CAD }; + + // Lambda to test multi-step payment with one of the MPTs not setting CanTransfer flag. + auto const testMultiStepMPTCanTransfer = [&](NoTransferMPT const noTransferMPT, + auto const& test) { + auto const getFlags = [&](NoTransferMPT const mpt) { + return mpt == noTransferMPT ? tfMPTCanTrade : kMptDexFlags; + }; + + Env env{*this, features}; + env.fund(XRP(1'000), gw, alice, carol, bob); + env.close(); + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::BTC)}); + MPTTester const eth( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::ETH)}); + MPTTester const usd( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::USD)}); + MPTTester const cad( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::CAD)}); + + env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); + env(offer(bob, usd(100), btc(100)), Txflags(tfPassive)); + env(offer(bob, btc(100), eth(100)), Txflags(tfPassive)); + env.close(); + + test(env, btc, eth, usd, cad); + }; + + // USD starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::USD, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + BEAST_EXPECT(expectOffers(env, bob, 3)); + + // fail - CAD/USD is owned by bob + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + + auto seq(env.seq(gw)); + env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); + env.close(); + // fail - CAD/USD is owned by bob + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + env.close(); + env(offerCancel(gw, seq)); + env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 3)); + // succeed - CAD/USD is owned by issuer + env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + // bob's CAD/USD is deleted. + BEAST_EXPECT(expectOffers(env, bob, 2)); + env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); + BEAST_EXPECT(expectOffers(env, gw, 0)); + }); + + // ETH starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::ETH, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + // fail - BTC/ETH is owned by bob, destination is carol + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 3)); + + // succeed - destination is an issuer + env(pay(alice, gw, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 3)); + }); + + // CAD starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::CAD, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + // fail - CAD/USD is owned by bob, source is alice + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + // succeed - source is the issuer + env(pay(gw, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); + env.close(); + // succeed - CAD/USD is owned by issuer + env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, gw, 0)); + BEAST_EXPECT(expectOffers(env, bob, 2)); + }); + + // BTC starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::BTC, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); + env.close(); + // succeed - USD/BTC is owned by issuer + env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, gw, 0)); + }); } // MPTCanTrade is not set @@ -5057,48 +4934,38 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol, bob}, .pay = 1'000, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanTransfer | tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .flags = kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanTransfer | tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .flags = kMptDexFlags}); env(pay(alice, carol, eth(1)), Path(~eth), Sendmax(btc(1)), Ter(tecNO_PERMISSION)); env(pay(alice, carol, btc(1)), Path(~btc), Sendmax(eth(1)), Ter(tecNO_PERMISSION)); env.close(); + // Enable MPTCanTrade so BTC can be crossed through offers. btc.set({.mutableFlags = tmfMPTSetCanTrade}); env(offer(bob, XRP(1), btc(1))); env(offer(bob, btc(1), eth(1))); env(offer(bob, eth(1), usd(1))); env.close(); - btc.set({.mutableFlags = tmfMPTClearCanTrade}); + BEAST_EXPECT(expectOffers(env, bob, 3)); + env(pay(gw, carol, usd(1)), Path(~btc, ~eth, ~usd), Sendmax(XRP(1)), - Txflags(tfPartialPayment | tfNoRippleDirect), - Ter(tecNO_PERMISSION)); + Txflags(tfPartialPayment | tfNoRippleDirect)); env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - - env(pay(carol, bob, btc(10)), Sendmax(XRP(10)), Ter(tecNO_PERMISSION)); - env(pay(carol, bob, XRP(10)), Sendmax(btc(10)), Ter(tecNO_PERMISSION)); - env(pay(gw, bob, btc(10)), Sendmax(XRP(10)), Ter(tecNO_PERMISSION)); - env(pay(gw, bob, XRP(10)), Sendmax(btc(10)), Ter(tecNO_PERMISSION)); - env(pay(carol, gw, btc(10)), Sendmax(XRP(10)), Ter(tecNO_PERMISSION)); - env(pay(carol, gw, XRP(10)), Sendmax(btc(10)), Ter(tecNO_PERMISSION)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); + BEAST_EXPECT(expectOffers(env, bob, 0)); } // Holders are locked @@ -6891,7 +6758,7 @@ class MPToken_test : public beast::unit_test::Suite .issuer = gw, .holders = {alice, carol}, .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); // src is issuer uint256 checkId{keylet::check(gw, env.seq(gw)).key}; @@ -6933,13 +6800,8 @@ class MPToken_test : public beast::unit_test::Suite env.close(); env(pay(gw, alice, mpt(10))); env.close(); - // can't cash - mpt.set({.account = gw, .mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - env(check::cash(carol, checkId, mpt(10)), Ter(tecNO_AUTH)); - env.close(); - // can cash - mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer}); + + // can cash since MPTCanTransfer is enabled env(check::cash(carol, checkId, mpt(10))); env.close(); } @@ -7358,296 +7220,332 @@ class MPToken_test : public beast::unit_test::Suite Env env(*this); env.fund(XRP(1'000'000), gw, alice, carol); - auto usd = MPTTester( - {.env = env, - .issuer = gw, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanTransfer | - tmfMPTCanMutateCanClawback | tmfMPTCanMutateCanTrade}); - auto eur = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000'000}); - auto const increment = env.current()->fees().increment; auto const txfee = Fee(drops(increment)); auto const badMPT = MPT(gw, 1'000); - auto createDeleteAMM = [&](Account const& lp) { - AMM amm( - env, - lp, - usd(1'000), - eur(1'000), - CreateArg{.fee = static_cast(increment.value())}); - amm.withdrawAll(lp); - BEAST_EXPECT(!amm.ammExists()); + auto const makeMPT = [&](std::uint32_t const flags, + Holders holders = {}, + std::uint64_t const pay = 0, + std::optional const mutableFlags = + std::nullopt) { + return MPTTester( + {.env = env, + .issuer = gw, + .holders = holders, + .pay = pay ? std::optional{pay} : std::nullopt, + .flags = flags, + .mutableFlags = mutableFlags}); + }; + + auto const makeDexMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { + return makeMPT( + tfMPTCanLock | kMptDexFlags, + holders, + pay, + tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanTransfer | + tmfMPTCanEnableCanTrade); + }; + + auto const makeNoTransferMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { + return makeMPT( + tfMPTCanLock | tfMPTCanTrade, holders, pay, tmfMPTCanEnableCanTransfer); + }; + + auto const makeNoTradeMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { + return makeMPT( + tfMPTCanLock | tfMPTCanTransfer, holders, pay, tmfMPTCanEnableCanTrade); }; - // // AMMCreate - // - - auto createJv = AMM::createJv(alice, badMPT(1'000), eur(1'000), 0); - - auto createFail = [&](Account const& account, auto const& err) { - createJv[sfAccount] = account.human(); - env(createJv, txfee, Ter(err)); - env.close(); - }; - - // MPTokenIssuance doesn't exist - - createFail(alice, tecOBJECT_NOT_FOUND); - - // MPToken doesn't exist - - createJv[sfAmount] = STAmount{usd(1'000)}.getJson(); - createFail(alice, tecNO_AUTH); - - // alice authorizes MPToken, can create - usd.authorize({.account = alice}); - env(pay(gw, alice, usd(1'000'000)), txfee); - env.close(); - createDeleteAMM(alice); - - // MPTLock is set - - // alice and issuer can't create - usd.set({.flags = tfMPTLock}); - createFail(alice, tecLOCKED); - createFail(gw, tecLOCKED); - - // MPTRequireAuth is set - - // alice is not authorized - usd.set({.flags = tfMPTUnlock}); - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); - createFail(alice, tecNO_AUTH); - // issuer can create - createDeleteAMM(gw); - - // alice is authorized, can create - usd.authorize({.account = gw, .holder = alice}); - createDeleteAMM(alice); - - // MPTCanTransfer is not set - - usd.set({.mutableFlags = tmfMPTClearRequireAuth}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // alice can't create - createFail(alice, tecNO_AUTH); - // issuer can create - createDeleteAMM(gw); - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - // alice can create - createDeleteAMM(alice); - - // MPTCanTrade is not set - - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - usd.set({.mutableFlags = tmfMPTClearCanTrade}); - // alice and issuer can't create - createFail(alice, tecNO_PERMISSION); - createFail(gw, tecNO_PERMISSION); - usd.set({.mutableFlags = tmfMPTSetCanTrade}); - - // - // AMMDeposit - // - - AMM amm(env, gw, usd(1'000), eur(1'000)); - - // MPTokenIssuance doesn't exist - - amm.deposit( - {.account = alice, - .asset1In = badMPT(1), - .asset2In = eur(1), - .assets = std::make_pair(badMPT, eur), - .err = Ter(terNO_AMM)}); - - // MPToken doesn't exist - - amm.deposit( - {.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)}); - - // MPTLock is set - - usd.set({.flags = tfMPTLock}); - // alice and issuer can't deposit - for (auto const& account : {carol, gw}) { + auto usd = makeDexMPT(); + auto eur = makeDexMPT({alice}, 1'000'000); + + auto createDeleteAMM = [&](auto const& asset, Account const& lp) { + AMM amm( + env, + lp, + asset(1'000), + eur(1'000), + CreateArg{.fee = static_cast(increment.value())}); + amm.withdrawAll(lp); + BEAST_EXPECT(!amm.ammExists()); + }; + + auto createFail = [&](auto const& asset, Account const& account, auto const& err) { + auto const createJv = AMM::createJv(account, asset(1'000), eur(1'000), 0); + env(createJv, txfee, Ter(err)); + env.close(); + }; + + // MPTokenIssuance doesn't exist + createFail(badMPT, alice, tecOBJECT_NOT_FOUND); + + // MPToken doesn't exist + createFail(usd, alice, tecNO_AUTH); + + // alice authorizes MPToken, can create + usd.authorize({.account = alice}); + env(pay(gw, alice, usd(1'000'000)), txfee); + env.close(); + createDeleteAMM(usd, alice); + + // MPTLock is set + // alice and issuer can't create + usd.set({.flags = tfMPTLock}); + createFail(usd, alice, tecLOCKED); + createFail(usd, gw, tecLOCKED); + + // MPTRequireAuth is set + // alice is not authorized + usd.set({.flags = tfMPTUnlock}); + usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + createFail(usd, alice, tecNO_AUTH); + // issuer can create + createDeleteAMM(usd, gw); + + // alice is authorized, can create + usd.authorize({.account = gw, .holder = alice}); + createDeleteAMM(usd, alice); + + // MPTCanTransfer is not set + { + auto usd2 = makeNoTransferMPT({alice}, 1'000'000); + + // alice can't create + createFail(usd2, alice, tecNO_AUTH); + // issuer can create + createDeleteAMM(usd2, gw); + usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + // alice can create + createDeleteAMM(usd2, alice); + } + + // MPTCanTrade is not set + { + auto usd3 = makeNoTradeMPT({alice}, 1'000'000); + + // alice and issuer can't create + createFail(usd3, alice, tecNO_PERMISSION); + createFail(usd3, gw, tecNO_PERMISSION); + usd3.set({.mutableFlags = tmfMPTSetCanTrade}); + // alice can create + createDeleteAMM(usd3, alice); + } + } + + // AMMDeposit + { + auto usd = makeDexMPT(); + auto eur = makeDexMPT({alice}, 1'000'000); + AMM amm(env, gw, usd(1'000), eur(1'000)); + + // MPTokenIssuance doesn't exist amm.deposit( - {.account = account, + {.account = alice, + .asset1In = badMPT(1), + .asset2In = eur(1), + .assets = std::make_pair(badMPT, eur), + .err = Ter(terNO_AMM)}); + + // MPToken doesn't exist + amm.deposit( + {.account = carol, .asset1In = usd(1), .asset2In = eur(1), - .err = Ter(tecLOCKED)}); + .err = Ter(tecNO_AUTH)}); + + // Fund carol for the AMMDeposit checks. + usd.authorize({.account = carol}); + env(pay(gw, carol, usd(1'000'000))); + eur.authorize({.account = carol}); + env(pay(gw, carol, eur(1'000'000))); + env.close(); + + // MPTLock is set + usd.set({.flags = tfMPTLock}); + + // alice and issuer can't deposit + for (auto const& account : {carol, gw}) + { + amm.deposit( + {.account = account, + .asset1In = usd(1), + .asset2In = eur(1), + .err = Ter(tecLOCKED)}); + amm.deposit( + {.account = account, + .asset1In = eur(1), + .assets = std::make_pair(eur, usd), + .err = Ter(tecLOCKED)}); + } + usd.set({.flags = tfMPTUnlock}); + + // MPTRequireAuth is set + // carol is not authorized by the issuer + usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + env.close(); amm.deposit( - {.account = account, + {.account = carol, + .asset1In = usd(1), + .asset2In = eur(1), + .err = Ter(tecNO_AUTH)}); + amm.deposit( + {.account = carol, .asset1In = eur(1), .assets = std::make_pair(eur, usd), - .err = Ter(tecLOCKED)}); + .err = Ter(tecNO_AUTH)}); + // issuer can deposit + amm.deposit({.account = gw, .tokens = 1'000}); + // carol is authorized, can deposit + usd.authorize({.account = gw, .holder = carol}); + amm.deposit({.account = carol, .tokens = 1'000}); + // Can't authorize or unauthorize AMM pseudo-account + usd.authorize( + {.account = gw, + .holder = Account{"amm", amm.ammAccount()}, + .err = tecNO_PERMISSION}); + usd.authorize( + {.account = gw, + .holder = Account{"amm", amm.ammAccount()}, + .flags = tfMPTUnauthorize, + .err = tecNO_PERMISSION}); + + // MPTCanTransfer is not set + { + auto usd2 = makeNoTransferMPT({carol}, 1'000'000); + AMM amm2(env, gw, usd2(1'000), eur(1'000)); + + // carol can't deposit + amm2.deposit( + {.account = carol, + .asset1In = usd2(1), + .asset2In = eur(1), + .err = Ter(tecNO_AUTH)}); + amm2.deposit( + {.account = carol, + .asset1In = eur(1), + .assets = std::make_pair(eur, usd2), + .err = Ter(tecNO_AUTH)}); + // issuer can deposit + amm2.deposit({.account = gw, .tokens = 1'000}); + usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + // carol can deposit + amm2.deposit({.account = carol, .tokens = 1'000}); + } } - usd.set({.flags = tfMPTUnlock}); - // MPTRequireAuth is set - - // carol authorizes MPToken but is not authorized by the issuer - usd.authorize({.account = carol}); - env(pay(gw, carol, usd(1'000'000))); - // carol authorizes EUR - eur.authorize({.account = carol}); - env(pay(gw, carol, eur(1'000'000))); - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); - env.close(); - amm.deposit( - {.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)}); - amm.deposit( - {.account = carol, - .asset1In = eur(1), - .assets = std::make_pair(eur, usd), - .err = Ter(tecNO_AUTH)}); - // issuer can deposit - amm.deposit({.account = gw, .tokens = 1'000}); - // carol is authorized, can deposit - usd.authorize({.account = gw, .holder = carol}); - amm.deposit({.account = carol, .tokens = 1'000}); - // Can't authorize or unauthorize AMM pseudo-account - usd.authorize( - {.account = gw, - .holder = Account{"amm", amm.ammAccount()}, - .err = tecNO_PERMISSION}); - usd.authorize( - {.account = gw, - .holder = Account{"amm", amm.ammAccount()}, - .flags = tfMPTUnauthorize, - .err = tecNO_PERMISSION}); - - // MPTCanTransfer is not set - - usd.set({.mutableFlags = tmfMPTClearRequireAuth}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // carol can't deposit - amm.deposit( - {.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)}); - amm.deposit( - {.account = carol, - .asset1In = eur(1), - .assets = std::make_pair(eur, usd), - .err = Ter(tecNO_AUTH)}); - // issuer can deposit - amm.deposit({.account = gw, .tokens = 1'000}); - // carol can deposit - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - amm.deposit({.account = carol, .tokens = 1'000}); - - // MPTCanTrade is not set - - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - usd.set({.mutableFlags = tmfMPTClearCanTrade}); - amm.deposit({.account = gw, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)}); - amm.deposit({.account = carol, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)}); - usd.set({.mutableFlags = tmfMPTSetCanTrade}); - - // // AMMWithdraw - // - - // MPTokenIssuance doesn't exist - - amm.withdraw( - WithdrawArg{ - .account = carol, - .asset1Out = badMPT(1), - .asset2Out = eur(1), - .assets = std::make_pair(badMPT, eur), - .err = Ter(terNO_AMM)}); - - // MPToken doesn't exist - doesn't apply since MPToken is created - // on withdraw in this case - - // MPTLock is set - - usd.set({.flags = tfMPTLock}); - // carol and issuer can't withdraw - for (auto const& account : {carol, gw}) { + auto usd = makeDexMPT(); + auto eur = makeDexMPT({carol}, 1'000'000); + AMM amm(env, gw, usd(1'000), eur(1'000)); + + usd.authorize({.account = carol}); + env(pay(gw, carol, usd(1'000'000))); + env.close(); + amm.deposit({.account = carol, .tokens = 1'000}); + + // MPTokenIssuance doesn't exist amm.withdraw( - {.account = account, + WithdrawArg{ + .account = carol, + .asset1Out = badMPT(1), + .asset2Out = eur(1), + .assets = std::make_pair(badMPT, eur), + .err = Ter(terNO_AMM)}); + + // MPToken doesn't exist - doesn't apply since MPToken is created + // on withdraw in this case + + // 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)}); + } + usd.set({.flags = tfMPTUnlock}); + + // MPTRequireAuth is set + usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + usd.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize}); + // carol can't withdraw + amm.withdraw( + {.account = carol, .asset1Out = usd(1), .asset2Out = eur(1), - .err = Ter(tecLOCKED)}); - amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecLOCKED)}); - // can single withdraw another asset + .err = Ter(tecNO_AUTH)}); + // can withdraw another asset amm.withdraw( - {.account = account, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + // issuer can withdraw + amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)}); + // carol is authorized, can withdraw + usd.authorize({.account = gw, .holder = carol}); + amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); + + // MPTCanTransfer is not set, allow to withdraw + { + auto usd2 = makeNoTransferMPT({carol}, 1'000'000); + AMM amm2(env, gw, usd2(1'000), eur(1'000)); + + // carol cannot deposit usd2 without MPTCanTransfer, so give her + // LP tokens directly to test the withdraw path. + env.trust(STAmount{amm2.lptIssue(), 1'000}, carol); + env(pay(gw, carol, STAmount{amm2.lptIssue(), 100})); + env.close(); + + // carol can withdraw + amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)}); + // can withdraw another asset + amm2.withdraw( + {.account = carol, + .asset1Out = eur(1), + .assets = std::make_pair(eur, usd2)}); + // issuer can withdraw + amm2.withdraw({.account = gw, .asset1Out = usd2(1), .asset2Out = eur(1)}); + // Holder can't transfer to another holder + env.fund(XRP(1'000), bob); + usd2.authorize({.account = bob}); + env(pay(carol, bob, usd2(1)), Ter(tecNO_AUTH)); + usd2.authorize({.account = bob, .flags = tfMPTUnauthorize}); + // Can redeem + env(pay(carol, gw, usd2(1))); + usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + // carol can withdraw + amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)}); + } + + // MPToken created on withdraw + { + auto usd3 = makeDexMPT(); + auto eur3 = makeDexMPT({carol}, 1'000'000); + AMM amm3(env, gw, usd3(1'000), eur3(1'000)); + + BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)) == nullptr); + // single-deposit EUR + amm3.deposit( + {.account = carol, + .asset1In = eur3(1'000), + .assets = std::make_pair(eur3, usd3)}); + BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)) == nullptr); + // withdraw in USD to create MPToken + amm3.withdraw({.account = carol, .asset1Out = usd3(100)}); + BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol))); + } } - usd.set({.flags = tfMPTUnlock}); - - // MPTRequireAuth is set - - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); - usd.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize}); - // carol can't withdraw - amm.withdraw( - {.account = carol, - .asset1Out = usd(1), - .asset2Out = eur(1), - .err = Ter(tecNO_AUTH)}); - // can withdraw another asset - amm.withdraw( - {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); - // issuer can withdraw - amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)}); - // carol is authorized, can withdraw - usd.authorize({.account = gw, .holder = carol}); - amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); - - // MPTCanTransfer is not set, allow to withdraw - - usd.set({.mutableFlags = tmfMPTClearRequireAuth}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // carol can withdraw - amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); - // can withdraw another asset - amm.withdraw( - {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); - // issuer can withdraw - amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)}); - // Holder can't transfer to another holder - env.fund(XRP(1'000), bob); - usd.authorize({.account = bob}); - env(pay(carol, bob, usd(1)), Ter(tecNO_AUTH)); - usd.authorize({.account = bob, .flags = tfMPTUnauthorize}); - // Can redeem - env(pay(carol, gw, usd(1))); - // carol can withdraw - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); - - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - - // MPTCanTrade is not set, allow to withdraw - - usd.set({.mutableFlags = tmfMPTClearCanTrade}); - amm.withdraw({.account = gw, .tokens = 1'000}); - amm.withdraw({.account = carol, .tokens = 1'000}); - // Can't DEX - amm.deposit( - DepositArg{.account = carol, .asset1In = usd(1), .err = Ter(tecNO_PERMISSION)}); - usd.set({.mutableFlags = tmfMPTSetCanTrade}); - - // MPToken created on withdraw - - // redeem all carol's USD and unauthorize USD - amm.withdrawAll(carol); - env(pay(carol, gw, env.balance(carol, usd))); - usd.authorize({.account = carol, .flags = tfMPTUnauthorize}); - BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), carol)) == nullptr); - // single-deposit EUR - amm.deposit( - {.account = carol, .asset1In = eur(1'000), .assets = std::make_pair(eur, usd)}); - // withdraw in USD to create MPToken - amm.withdraw({.account = carol, .asset1Out = usd(100)}); - BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), carol))); } } @@ -7707,41 +7605,37 @@ class MPToken_test : public beast::unit_test::Suite Env env(*this); env.fund(XRP(1'000), gw, alice, carol); - MPTTester mpt( - {.env = env, - .issuer = gw, - .holders = {alice, carol}, - .pay = 100, - .flags = kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanTrade}); + auto const checkCanTradeCanTransfer = [&](std::uint32_t const flags, + TER const gwToGw, + TER const gwToAlice, + TER const aliceToAlice, + TER const aliceToCarol) { + MPTTester const mpt( + {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, .flags = flags}); + + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == gwToGw); + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == gwToAlice); + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == aliceToAlice); + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == aliceToCarol); + }; // Both flags are enabled - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol))); + checkCanTradeCanTransfer(kMptDexFlags, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS); // MPTCanTrade is disabled - mpt.set({.mutableFlags = tmfMPTClearCanTrade}); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_PERMISSION); + checkCanTradeCanTransfer( + tfMPTCanTransfer, + tecNO_PERMISSION, + tecNO_PERMISSION, + tecNO_PERMISSION, + tecNO_PERMISSION); // MPTCanTransfer is disabled - mpt.set({.mutableFlags = tmfMPTSetCanTrade}); - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice))); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_AUTH); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_AUTH); + checkCanTradeCanTransfer(tfMPTCanTrade, tesSUCCESS, tesSUCCESS, tecNO_AUTH, tecNO_AUTH); // Both flags are disabled - mpt.set({.mutableFlags = tmfMPTClearCanTrade}); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_PERMISSION); + checkCanTradeCanTransfer( + 0, tecNO_PERMISSION, tecNO_PERMISSION, tecNO_PERMISSION, tecNO_PERMISSION); } public: diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 2c83ad91ec..065b6b0044 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1599,7 +1599,7 @@ class Vault_test : public beast::unit_test::Suite {.flags = tfMPTCanTransfer | tfMPTCanLock | (args.enableClawback ? tfMPTCanClawback : kNone) | (args.requireAuth ? tfMPTRequireAuth : kNone), - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); PrettyAsset const asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); @@ -2238,149 +2238,6 @@ class Vault_test : public beast::unit_test::Suite env.close(); } - testCase([this]( - Env& env, - Account const&, - Account const& owner, - Account const& depositor, - PrettyAsset const& asset, - Vault& vault, - MPTTester& mptt) { - testcase("MPT non-transferable: block deposit, allow withdraw"); - - 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(); - - // Issuer governance: clear CanTransfer. New exposure must be - // blocked, but recovery paths must remain open so existing - // depositors are not trapped. - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // New deposit is blocked. - env(tx, Ter{tecNO_AUTH}); - env.close(); - - // Existing depositor can always withdraw, even though the asset - // is no longer freely transferable. - tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx); - env.close(); - - // Delete vault with zero balance - env(vault.del({.owner = owner, .id = keylet.key})); - }); - - { - testcase("MPT non-transferable: pre-fixCleanup3_2_0 withdraw blocked"); - - // Regression: before fixCleanup3_2_0 a depositor was trapped if - // the issuer cleared lsfMPTCanTransfer. Verify that the legacy - // (broken) behavior is preserved when the amendment is disabled. - Env env{*this, testableAmendments() - fixCleanup3_2_0}; - Account const issuer{"issuer"}; - Account const owner{"owner"}; - Account const depositor{"depositor"}; - env.fund(XRP(10'000), issuer, owner, depositor); - env.close(); - Vault const vault{env}; - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mptt.issuanceID(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = depositor}); - env(pay(issuer, depositor, asset(1'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)})); - env.close(); - - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Pre-amendment: deposit blocked (matches new behavior). - env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}), - Ter{tecNO_AUTH}); - env.close(); - - // Pre-amendment: withdraw is also blocked - this is the bug - // that fixCleanup3_2_0 fixes. - env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}), - Ter{tecNO_AUTH}); - env.close(); - } - - { - testcase("MPT non-transferable: vault shares inherit restriction"); - - Env env{*this, testableAmendments()}; - Account const issuer{"issuer"}; - Account const owner{"owner"}; - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10'000), issuer, owner, alice, bob); - env.close(); - Vault const vault{env}; - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mptt.issuanceID(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = alice}); - mptt.authorize({.account = bob}); - env(pay(issuer, alice, asset(1'000))); - env(pay(issuer, bob, asset(1'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)})); - // Bob also deposits so he has a share MPToken to receive into. - env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)})); - env.close(); - - auto const shares = [&]() -> PrettyAsset { - auto const sle = env.le(keylet); - BEAST_EXPECT(sle != nullptr); - return MPTIssue(sle->at(sfShareMPTID)); - }(); - - // Sanity: while CanTransfer is set on the underlying, peer-to-peer - // share transfers are allowed. - env(pay(alice, bob, shares(1))); - env.close(); - - // Issuer governance: clear CanTransfer on the underlying. - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Vault shares inherit the restriction: third-party share-to-share - // payments are blocked. - env(pay(alice, bob, shares(1)), Ter{tecNO_AUTH}); - env.close(); - - // Recovery path: existing share holders can still redeem shares - // for the underlying asset via VaultWithdraw. - env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = shares(1)})); - env.close(); - } - { testcase("MPT locked: vault shares inherit underlying lock"); @@ -2458,56 +2315,6 @@ class Vault_test : public beast::unit_test::Suite BEAST_EXPECT(expectOffers(env, alice, 1)); } - { - testcase("MPT non-transferable: pre-fixCleanup3_2_0 share transfer succeeds"); - - // Regression: before fixCleanup3_2_0 a peer-to-peer share Payment - // succeeded even when the underlying asset's lsfMPTCanTransfer - // was cleared. Verify that the legacy (non-inheriting) behavior - // is preserved when the amendment is disabled. - Env env{*this, testableAmendments() - fixCleanup3_2_0}; - Account const issuer{"issuer"}; - Account const owner{"owner"}; - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10'000), issuer, owner, alice, bob); - env.close(); - Vault const vault{env}; - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mptt.issuanceID(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = alice}); - mptt.authorize({.account = bob}); - env(pay(issuer, alice, asset(1'000))); - env(pay(issuer, bob, asset(1'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)})); - env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)})); - env.close(); - - auto const shares = [&]() -> PrettyAsset { - auto const sle = env.le(keylet); - BEAST_EXPECT(sle != nullptr); - return MPTIssue(sle->at(sfShareMPTID)); - }(); - - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Pre-amendment: share transfer leaks past underlying restriction. - env(pay(alice, bob, shares(1))); - env.close(); - } - { testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM"); @@ -2522,8 +2329,8 @@ class Vault_test : public beast::unit_test::Suite MPTTester mptt{env, issuer, kMptInitNoFund}; mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTrade}); + {.flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCanEnableCanTrade}); PrettyAsset const asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = alice}); @@ -2547,38 +2354,18 @@ class Vault_test : public beast::unit_test::Suite return MPTIssue(sle->at(sfShareMPTID)); }(); - // Sanity: while CanTrade is set on the underlying, both the asset - // and the vault share can be placed on the DEX. - env(offer(alice, XRP(1), asset(10))); - env(offer(alice, XRP(1), shares(1))); - env.close(); - - // Issuer governance: clear CanTrade on the underlying. - mptt.set({.mutableFlags = tmfMPTClearCanTrade}); - env.close(); - - // Control: clearing CanTrade on the underlying is observable on - // the DEX path for that asset. + // CanTrade is not set on the underlying, both the asset and + // the vault share are blocked on the DEX. env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); + env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION}); env.close(); - // Control: clearing CanTrade on the underlying is also observable - // on the AMM path for that asset. - AMM const ammUnderlyingFails( + // The inherited CanTrade restriction also blocks AMM creation. + AMM const ammUnderlyingFail( env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION}); - - // Post-fixCleanup3_2_0: vault shares inherit the underlying's - // CanTrade restriction on the DEX path (canTrade reads the - // share's sfReferenceHolding and dispatches to the underlying). - env(offer(bob, XRP(1), shares(1)), Ter{tecNO_PERMISSION}); - env.close(); - - // checkMPTAllowed mirrors the inheritance for AMM/Offer- - // crossing/Check paths, so a share AMM also cannot be created - // when the underlying CanTrade is cleared. AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION}); - // Deposit still works (canAddHolding does not consult the field). + // Deposit still works before enabling CanTrade. env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)})); env.close(); @@ -2587,9 +2374,19 @@ class Vault_test : public beast::unit_test::Suite env(pay(alice, bob, shares(1))); env.close(); - // Withdraw still works. + // Withdraw still works before enabling CanTrade. env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)})); env.close(); + + // Enable CanTrade on the underlying. + mptt.set({.mutableFlags = tmfMPTSetCanTrade}); + env.close(); + + env(offer(alice, XRP(1), asset(10))); + env(offer(alice, XRP(1), shares(1))); + env.close(); + + AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000)); } { diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 1e127e7c05..7da3305eec 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,21 @@ namespace xrpl::test::jtx { +struct MPTSetFlagMapping +{ + std::uint32_t setFlag; + std::uint32_t ledgerFlag; +}; + +static constexpr std::array mptSetFlagMappings = {{ + {.setFlag = tmfMPTSetCanLock, .ledgerFlag = lsfMPTCanLock}, + {.setFlag = tmfMPTSetRequireAuth, .ledgerFlag = lsfMPTRequireAuth}, + {.setFlag = tmfMPTSetCanEscrow, .ledgerFlag = lsfMPTCanEscrow}, + {.setFlag = tmfMPTSetCanClawback, .ledgerFlag = lsfMPTCanClawback}, + {.setFlag = tmfMPTSetCanTrade, .ledgerFlag = lsfMPTCanTrade}, + {.setFlag = tmfMPTSetCanTransfer, .ledgerFlag = lsfMPTCanTransfer}, +}}; + void MptFlags::operator()(Env& env) const { @@ -424,58 +440,12 @@ MPTTester::set(MPTSet const& arg) if (arg.mutableFlags) { - if (*arg.mutableFlags & tmfMPTSetCanLock) + for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings) { - flags |= lsfMPTCanLock; - } - else if (*arg.mutableFlags & tmfMPTClearCanLock) - { - flags &= ~lsfMPTCanLock; - } - - if (*arg.mutableFlags & tmfMPTSetRequireAuth) - { - flags |= lsfMPTRequireAuth; - } - else if (*arg.mutableFlags & tmfMPTClearRequireAuth) - { - flags &= ~lsfMPTRequireAuth; - } - - if (*arg.mutableFlags & tmfMPTSetCanEscrow) - { - flags |= lsfMPTCanEscrow; - } - else if (*arg.mutableFlags & tmfMPTClearCanEscrow) - { - flags &= ~lsfMPTCanEscrow; - } - - if (*arg.mutableFlags & tmfMPTSetCanClawback) - { - flags |= lsfMPTCanClawback; - } - else if (*arg.mutableFlags & tmfMPTClearCanClawback) - { - flags &= ~lsfMPTCanClawback; - } - - if (*arg.mutableFlags & tmfMPTSetCanTrade) - { - flags |= lsfMPTCanTrade; - } - else if (*arg.mutableFlags & tmfMPTClearCanTrade) - { - flags &= ~lsfMPTCanTrade; - } - - if (*arg.mutableFlags & tmfMPTSetCanTransfer) - { - flags |= lsfMPTCanTransfer; - } - else if (*arg.mutableFlags & tmfMPTClearCanTransfer) - { - flags &= ~lsfMPTCanTransfer; + if ((*arg.mutableFlags & setFlag) != 0u) + { + flags |= ledgerFlag; + } } } } From 93eab33dc23e0a026c55234ae7b5190bb195d93e Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:45:42 -0400 Subject: [PATCH 038/121] fix: Improve ValidAMM invariant (#7295) --- include/xrpl/tx/invariants/AMMInvariant.h | 11 ++- src/libxrpl/tx/invariants/AMMInvariant.cpp | 92 +++++++++++++++++++--- src/test/app/Invariants_test.cpp | 84 ++++++++++++++++++++ 3 files changed, 175 insertions(+), 12 deletions(-) diff --git a/include/xrpl/tx/invariants/AMMInvariant.h b/include/xrpl/tx/invariants/AMMInvariant.h index ee2fb66a1c..4b56370774 100644 --- a/include/xrpl/tx/invariants/AMMInvariant.h +++ b/include/xrpl/tx/invariants/AMMInvariant.h @@ -15,7 +15,9 @@ class ValidAMM std::optional ammAccount_; std::optional lptAMMBalanceAfter_; std::optional lptAMMBalanceBefore_; + std::optional lptAMMBalanceBeforeDeletion_; bool ammPoolChanged_{false}; + bool ammDeleted_{false}; public: enum class ZeroAllowed : bool { No = false, Yes = true }; @@ -35,12 +37,17 @@ private: [[nodiscard]] bool finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; [[nodiscard]] bool - finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + finalizeDelete(bool enforce, bool enforceAMMDelete, TER res, beast::Journal const&) const; [[nodiscard]] bool finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; // Includes clawback [[nodiscard]] bool - finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + finalizeWithdraw( + STTx const&, + ReadView const&, + bool enforce, + bool enforceAMMDelete, + beast::Journal const&) const; [[nodiscard]] bool finalizeDEX(bool enforce, beast::Journal const&) const; [[nodiscard]] bool diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp b/src/libxrpl/tx/invariants/AMMInvariant.cpp index ecd7bedf89..356c26d6b0 100644 --- a/src/libxrpl/tx/invariants/AMMInvariant.cpp +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp @@ -27,7 +27,14 @@ void ValidAMM::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { if (isDelete) + { + if (before && before->getType() == ltAMM) + { + ammDeleted_ = true; + lptAMMBalanceBeforeDeletion_ = before->getFieldAmount(sfLPTokenBalance); + } return; + } if (after) { @@ -166,18 +173,60 @@ ValidAMM::finalizeCreate( } bool -ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +ValidAMM::finalizeDelete(bool enforce, bool enforceAMMDelete, TER res, beast::Journal const& j) + const { if (ammAccount_) { // LCOV_EXCL_START - std::string const msg = (isTesSuccess(res)) ? "AMM object is not deleted on tesSUCCESS" - : "AMM object is changed on tecINCOMPLETE"; + std::string const msg = (isTesSuccess(res)) ? "AMM object remained on tesSUCCESS" + : "AMM object changed on tecINCOMPLETE"; JLOG(j.error()) << "Invariant failed: AMMDelete failed, " << msg; if (enforce) return false; // LCOV_EXCL_STOP } + if (enforceAMMDelete) + { + if (isTesSuccess(res)) + { + if (!ammDeleted_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS"; + return false; + // LCOV_EXCL_STOP + } + if (!lptAMMBalanceBeforeDeletion_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "Invariant failed: AMMDelete failed, AMM object deleted without LP balance"; + return false; + // LCOV_EXCL_STOP + } + if (*lptAMMBalanceBeforeDeletion_ != beast::kZero) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP " + "balance: " + << *lptAMMBalanceBeforeDeletion_; + return false; + // LCOV_EXCL_STOP + } + } + else if (ammDeleted_) + { + // AMM should only be fully deleted when AMMDelete returns tesSUCCESS. + // LCOV_EXCL_START + JLOG(j.error()) << "Invariant failed: AMMDelete failed, AMM object deleted when result " + "is not tesSUCCESS"; + return false; + // LCOV_EXCL_STOP + } + } return true; } @@ -271,16 +320,20 @@ ValidAMM::finalizeWithdraw( xrpl::STTx const& tx, xrpl::ReadView const& view, bool enforce, + bool enforceAMMDelete, beast::Journal const& j) const { - if (!ammAccount_) + if (enforceAMMDelete && ammDeleted_) { - // Last Withdraw or Clawback deleted AMM + // Last Withdraw or Clawback can delete the AMM. We don't have to check + // the LPToken balance because a final AMMWithdraw or AMMClawback can + // redeem the remaining LP tokens and delete the AMM entry in the same + // transaction. + return true; } - else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + if (ammAccount_ && !generalInvariant(tx, view, ZeroAllowed::Yes, j) && enforce) { - if (enforce) - return false; + return false; } return true; @@ -300,6 +353,25 @@ ValidAMM::finalize( return true; bool const enforce = view.rules().enabled(fixAMMv1_3); + bool const enforceAMMDelete = view.rules().enabled(fixCleanup3_3_0); + + // AMM can only be deleted by AMMWithdraw, AMMClawback, and AMMDelete + if (enforceAMMDelete && ammDeleted_) + { + switch (tx.getTxnType()) + { + case ttAMM_WITHDRAW: + case ttAMM_CLAWBACK: + case ttAMM_DELETE: + break; + default: + // LCOV_EXCL_START + JLOG(j.error()) << "Invariant failed: AMM failed, unexpected AMM deletion by " + << tx.getTxnType(); + return false; + // LCOV_EXCL_STOP + } + } switch (tx.getTxnType()) { @@ -309,13 +381,13 @@ ValidAMM::finalize( return finalizeDeposit(tx, view, enforce, j); case ttAMM_CLAWBACK: case ttAMM_WITHDRAW: - return finalizeWithdraw(tx, view, enforce, j); + return finalizeWithdraw(tx, view, enforce, enforceAMMDelete, j); case ttAMM_BID: return finalizeBid(enforce, j); case ttAMM_VOTE: return finalizeVote(enforce, j); case ttAMM_DELETE: - return finalizeDelete(enforce, result, j); + return finalizeDelete(enforce, enforceAMMDelete, result, j); case ttCHECK_CASH: case ttOFFER_CREATE: case ttPAYMENT: diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 6d53d25661..e0c29ea72a 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include @@ -1277,6 +1278,87 @@ class Invariants_test : public beast::unit_test::Suite }); } + void + testAMMDeleteInvariants(FeatureBitset features) + { + using namespace test::jtx; + + bool const enforceAMMDelete = features[fixCleanup3_3_0]; + testcase << "AMM delete invariants" + std::string(enforceAMMDelete ? " fix" : ""); + + Env env(*this, features); + Account const issuer{"issuer"}; + Issue const lptIssue{Currency(0x4c50540000000000), issuer.id()}; + STAmount const zeroLP{lptIssue, 0}; + STAmount const nonZeroLP{lptIssue, 1}; + + auto const makeAMM = [](STAmount const& lptBalance) { + auto sleAMM = std::make_shared(keylet::amm(uint256(1))); + sleAMM->setFieldAmount(sfLPTokenBalance, lptBalance); + return sleAMM; + }; + + auto const checkInvariant = [&](TxType txType, + TER result, + std::optional const& deletedLPBalance, + bool expected, + std::string const& expectedLog) { + test::StreamSink sink{beast::Severity::Warning}; + beast::Journal const jlog{sink}; + ValidAMM invariant; + + if (deletedLPBalance) + invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr); + + bool const actual = invariant.finalize( + STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog); + + BEAST_EXPECTS(actual == expected, "unexpected AMM delete invariant result"); + auto const messages = sink.messages().str(); + auto const expectedLogWhenEnforced = enforceAMMDelete ? expectedLog : ""; + if (!expectedLogWhenEnforced.empty()) + { + BEAST_EXPECTS(messages.contains(expectedLogWhenEnforced), expectedLogWhenEnforced); + } + else + { + BEAST_EXPECTS(messages.empty(), messages); + } + }; + + checkInvariant( + ttPAYMENT, + tesSUCCESS, + nonZeroLP, + !enforceAMMDelete, + "Invariant failed: AMM failed, unexpected AMM deletion by"); + checkInvariant( + ttAMM_DELETE, + tesSUCCESS, + std::nullopt, + !enforceAMMDelete, + "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS"); + checkInvariant( + ttAMM_DELETE, + tesSUCCESS, + nonZeroLP, + !enforceAMMDelete, + "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP balance"); + checkInvariant( + ttAMM_DELETE, + tecINCOMPLETE, + zeroLP, + !enforceAMMDelete, + "Invariant failed: AMMDelete failed, AMM object deleted when result is not tesSUCCESS"); + + checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, nonZeroLP, true, ""); + checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, nonZeroLP, true, ""); + + checkInvariant(ttAMM_DELETE, tesSUCCESS, zeroLP, true, ""); + checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, zeroLP, true, ""); + checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, zeroLP, true, ""); + } + static SLE::pointer createPermissionedDomain( ApplyContext& ac, @@ -4900,6 +4982,8 @@ public: testNoZeroEscrow(); testValidNewAccountRoot(); testNFTokenPageInvariants(); + testAMMDeleteInvariants(defaultAmendments()); + testAMMDeleteInvariants(defaultAmendments() - fixCleanup3_3_0); testPermissionedDomainInvariants(defaultAmendments() | fixCleanup3_1_3); testPermissionedDomainInvariants(defaultAmendments() - fixCleanup3_1_3); testPermissionedDEX(defaultAmendments() | fixCleanup3_1_3); From 19a9ed776761ee738da4ee2ffde88aa349a9ae31 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:42:57 -0400 Subject: [PATCH 039/121] fix: Move AMMInvariant weakInvariantCheck logic into the transaction (#7032) --- include/xrpl/ledger/helpers/AMMHelpers.h | 26 +++++++++++++ src/libxrpl/ledger/helpers/AMMHelpers.cpp | 37 +++++++++++++++++++ src/libxrpl/tx/invariants/AMMInvariant.cpp | 9 +---- .../tx/transactors/dex/AMMClawback.cpp | 10 +++++ src/libxrpl/tx/transactors/dex/AMMDeposit.cpp | 13 +++++++ .../tx/transactors/dex/AMMWithdraw.cpp | 10 +++++ src/test/app/AMMClawback_test.cpp | 18 ++++++++- src/test/app/AMM_test.cpp | 16 ++++++-- 8 files changed, 127 insertions(+), 12 deletions(-) diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index d21e50e7cb..de8bb9d3f7 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -37,6 +37,8 @@ reduceOffer(auto const& amount) enum class IsDeposit : bool { No = false, Yes = true }; +inline Number const kAMMInvariantRelativeTolerance{1, -11}; + /** Calculate LP Tokens given AMM pool reserves. * @param asset1 AMM one side of the pool reserve * @param asset2 AMM another side of the pool reserve @@ -738,6 +740,30 @@ ammPoolHolds( AuthHandling authHandling, beast::Journal const j); +/** Check AMM pool product invariant after an AMM operation that changes LP tokens + * (deposit/withdraw/clawback) from an already calculated pool product mean. + * Returns tecPRECISION_LOSS if poolProductMean < newLPTokenBalance beyond the + * invariant tolerance, + * tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal). + */ +TER +checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance); + +/** Check AMM pool product invariant after an AMM operation that changes LP tokens + * (deposit/withdraw/clawback). + * Returns tecPRECISION_LOSS if sqrt(asset1 * asset2) < newLPTokenBalance beyond + * the invariant tolerance, + * tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal). + */ +TER +checkAMMPrecisionLoss( + ReadView const& view, + AccountID const& ammAccountID, + Asset const& asset1, + Asset const& asset2, + STAmount const& newLPTokenBalance, + beast::Journal const j); + /** Get AMM pool and LP token balances. If both optIssue are * provided then they are used as the AMM token pair issues. * Otherwise the missing issues are fetched from ammSle. diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp index a59b8e4436..cacbfc9d58 100644 --- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp @@ -433,6 +433,43 @@ ammPoolHolds( return std::make_pair(assetInBalance, assetOutBalance); } +TER +checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance) +{ + if (newLPTokenBalance <= beast::kZero) + return tesSUCCESS; + if (poolProductMean >= newLPTokenBalance) + return tesSUCCESS; + // Strong check failed. Allow the same relative tolerance as the invariant + // checker's weak check. Only return tecPRECISION_LOSS when both fail. + if (withinRelativeDistance( + poolProductMean, Number{newLPTokenBalance}, kAMMInvariantRelativeTolerance)) + return tesSUCCESS; + return tecPRECISION_LOSS; +} + +TER +checkAMMPrecisionLoss( + ReadView const& view, + AccountID const& ammAccountID, + Asset const& asset1, + Asset const& asset2, + STAmount const& newLPTokenBalance, + beast::Journal const j) +{ + if (newLPTokenBalance <= beast::kZero) + return tesSUCCESS; + auto const [amount, amount2] = ammPoolHolds( + view, + ammAccountID, + asset1, + asset2, + FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, + j); + return checkAMMPrecisionLoss(root2(amount * amount2), newLPTokenBalance); +} + std::expected, TER> ammHolds( ReadView const& view, diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp b/src/libxrpl/tx/invariants/AMMInvariant.cpp index 356c26d6b0..cca0ce149c 100644 --- a/src/libxrpl/tx/invariants/AMMInvariant.cpp +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp @@ -270,13 +270,8 @@ ValidAMM::generalInvariant( auto const poolProductMean = root2(amount * amount2); bool const nonNegativeBalances = validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); - bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; - // Allow for a small relative error if strongInvariantCheck fails - auto weakInvariantCheck = [&]() { - return *lptAMMBalanceAfter_ != beast::kZero && - withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); - }; - if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck())) + auto const precisionLoss = checkAMMPrecisionLoss(poolProductMean, *lptAMMBalanceAfter_); + if (!nonNegativeBalances || !isTesSuccess(precisionLoss)) { JLOG(j.error()) << "Invariant failed: AMM " << tx.getTxnType() << " " << tx.getHash(HashPrefix::TransactionId) << " " << ammPoolChanged_ << " " diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index b94e97e931..0cc2be381f 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -258,6 +258,16 @@ AMMClawback::applyGuts(Sandbox& sb) if (!isTesSuccess(result)) return result; // LCOV_EXCL_LINE + if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) + { + if (auto const ter = + checkAMMPrecisionLoss(sb, ammAccount, asset, asset2, newLPTokenBalance, j_); + !isTesSuccess(ter)) + { + return ter; + } + } + auto const res = AMMWithdraw::deleteAMMAccountIfEmpty(sb, ammSle, newLPTokenBalance, asset, asset2, j_); if (!res.second) diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 91858e3cd7..653e8c6961 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -470,6 +470,19 @@ AMMDeposit::applyGuts(Sandbox& sb) XRPL_ASSERT( newLPTokenBalance > beast::kZero, "xrpl::AMMDeposit::applyGuts : valid new LP token balance"); + // Defensive check: deposit formulas with fixAMMv1_3 round LP tokens + // down and asset amounts up, so sqrt(pool1*pool2) >= newLPTokenBalance + // is guaranteed to hold. A precision loss failure is not expected. + if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) + { + if (auto const ter = checkAMMPrecisionLoss( + sb, ammAccountID, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], newLPTokenBalance, j_); + !isTesSuccess(ter)) + { + UNREACHABLE("xrpl::AMMDeposit::applyGuts : AMM precision loss"); + return {ter, false}; // LCOV_EXCL_LINE + } + } ammSle->setFieldAmount(sfLPTokenBalance, newLPTokenBalance); // LP depositing into AMM empty state gets the auction slot // and the voting diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index d3a6c9c74c..17ce1a6b83 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -406,6 +406,16 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (!isTesSuccess(result)) return {result, false}; + if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) + { + if (auto const ter = checkAMMPrecisionLoss( + sb, ammAccountID, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], newLPTokenBalance, j_); + !isTesSuccess(ter)) + { + return {ter, false}; + } + } + auto const res = deleteAMMAccountIfEmpty( sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); // LCOV_EXCL_START diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 9683e8ac17..ba416d8192 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2486,8 +2486,17 @@ class AMMClawback_test : public beast::unit_test::Suite else if (!features[fixAMMClawbackRounding]) { // sqrt(amount * amount2) >= LPTokens and exceeds the allowed - // tolerance - env(amm::ammClawback(gw, alice, usd, eur, usd(1)), Ter(tecINVARIANT_FAILED)); + // tolerance. + // With fixCleanup3_3_0 this is caught in the transaction layer; + // without it the invariant checker fires instead. + if (features[fixCleanup3_3_0]) + { + env(amm::ammClawback(gw, alice, usd, eur, usd(1)), Ter(tecPRECISION_LOSS)); + } + else + { + env(amm::ammClawback(gw, alice, usd, eur, usd(1)), Ter(tecINVARIANT_FAILED)); + } BEAST_EXPECT(amm.ammExists()); } else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) @@ -2514,6 +2523,11 @@ class AMMClawback_test : public beast::unit_test::Suite testFeatureDisabled(all - featureAMMClawback); for (auto const& features : {all - fixAMMv1_3 - fixAMMClawbackRounding - featureMPTokensV2, + // fixAMMv1_3 on, fixAMMClawbackRounding off, fixCleanup3_3_0 off: + // precision loss caught by invariant checker -> tecINVARIANT_FAILED + all - fixAMMClawbackRounding - fixCleanup3_3_0 - featureMPTokensV2, + // fixAMMv1_3 on, fixAMMClawbackRounding off, fixCleanup3_3_0 on: + // precision loss caught in transaction layer -> tecPRECISION_LOSS all - fixAMMClawbackRounding - featureMPTokensV2, all - featureMPTokensV2, all}) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 1b54c2aab9..b01d58ddff 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -1842,8 +1842,18 @@ private: // are rounded to all LP tokens. testAMM( [&](AMM& ammAlice, Env& env) { - auto const err = - env.enabled(fixAMMv1_3) ? Ter(tecINVARIANT_FAILED) : Ter(tecAMM_BALANCE); + // Without fixAMMv1_3: sub-method returns tecAMM_BALANCE early. + // With fixAMMv1_3 but without fixCleanup3_3_0: sub-method succeeds + // but invariant check catches the precision violation. + // With fixCleanup3_3_0: caught in the transaction layer before + // the invariant checker runs. + auto const err = [&] { + if (!env.enabled(fixAMMv1_3)) + return Ter(tecAMM_BALANCE); + if (env.enabled(fixCleanup3_3_0)) + return Ter(tecPRECISION_LOSS); + return Ter(tecINVARIANT_FAILED); + }(); ammAlice.withdraw( alice_, STAmount{USD, UINT64_C(9'999'999999999999), -12}, @@ -1851,7 +1861,7 @@ private: std::nullopt, err); }, - {.features = {all, all - fixAMMv1_3}, .noLog = true}); + {.features = {all, all - fixAMMv1_3, all - fixCleanup3_3_0}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { From dd7401fde21c6ddd5d5e1ef4761a3de6630b40fa Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 22 Jun 2026 14:44:42 -0400 Subject: [PATCH 040/121] refactor: Clean up tec object deletion logic (#6588) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- include/xrpl/tx/Transactor.h | 7 + src/libxrpl/tx/Transactor.cpp | 238 ++++++++++++++-------------- src/test/app/NFToken_test.cpp | 2 + src/test/app/Offer_test.cpp | 25 +++ src/test/app/SetRegularKey_test.cpp | 22 +++ 5 files changed, 172 insertions(+), 122 deletions(-) diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index a27d638107..470571eb48 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -7,6 +7,7 @@ #include #include +#include #include namespace xrpl { @@ -419,8 +420,13 @@ private: TER consumeSeqProxy(SLE::pointer const& sleAccount); + TER payFee(); + + std::tuple + processPersistentChanges(TER result, XRPAmount fee); + static NotTEC checkSingleSign( ReadView const& view, @@ -428,6 +434,7 @@ private: AccountID const& idAccount, SLE::const_pointer sleAccount, beast::Journal const j); + static NotTEC checkMultiSign( ReadView const& view, diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index b57d30d2b3..2ff24d92b5 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -45,8 +45,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -1078,26 +1080,6 @@ removeDeletedTrustLines( } } -static void -removeDeletedMPTs(ApplyView& view, std::vector const& mpts, beast::Journal viewJ) -{ - // There could be at most two MPTs - one for each side of AMM pool - if (mpts.size() > 2) - { - JLOG(viewJ.error()) << "removeDeletedMPTs: deleted mpts exceed 2 " << mpts.size(); - return; - } - - for (auto const& index : mpts) - { - if (auto const sleState = view.peek({ltMPTOKEN, index}); sleState && - deleteAMMMPToken(view, sleState, (*sleState)[sfIssuer], viewJ) != tesSUCCESS) - { - JLOG(viewJ.error()) << "removeDeletedMPTs: failed to delete AMM MPT"; - } - } -} - /** Reset the context, discarding any changes made and adjust the fee. @param fee The transaction fee to be charged. @@ -1160,6 +1142,118 @@ Transactor::trapTransaction(uint256 txHash) const JLOG(j_.debug()) << "Transaction trapped: " << txHash; } +std::tuple +Transactor::processPersistentChanges(TER result, XRPAmount fee) +{ + JLOG(j_.trace()) << "reapplying because of " << transToken(result); + + // FIXME: This mechanism for doing work while returning a `tec` is + // awkward and very limiting. A more general purpose approach + // should be used, making it possible to do more useful work + // when transactions fail with a `tec` code. + + auto typesForResult = [](TER const ter) { + std::unordered_set types; + if ((ter == tecOVERSIZE) || (ter == tecKILLED)) + { + types.insert(ltOFFER); + } + else if (ter == tecINCOMPLETE) + { + types.insert(ltRIPPLE_STATE); + } + else if (ter == tecEXPIRED) + { + types.insert(ltNFTOKEN_OFFER); + types.insert(ltCREDENTIAL); + } + return types; + }; + + // Build a list of ledger entry types to collect, based on the + // result code. Only deleted objects of these types will be + // re-applied after the context is reset. + auto const typesToCollect = typesForResult(result); + + std::map> deletedObjects; + if (!typesToCollect.empty()) + { + ctx_.visit( + [&typesToCollect, &deletedObjects]( + uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) { + if (isDelete) + { + XRPL_ASSERT( + before && after, + "xrpl::Transactor::processPersistentChanges : non-null " + "SLE inputs"); + if (before && after) + { + auto const type = before->getType(); + if (typesToCollect.contains(type)) + { + // For offers, only collect unfunded removals + // (where TakerPays is unchanged) + if (type == ltOFFER && + before->getFieldAmount(sfTakerPays) != + after->getFieldAmount(sfTakerPays)) + return; + + deletedObjects[type].push_back(index); + } + } + } + }); + } + + // Reset the context, potentially adjusting the fee. + { + auto const resetResult = reset(fee); + if (!isTesSuccess(resetResult.first)) + result = resetResult.first; + + fee = resetResult.second; + } + + // Re-apply the collected deletions, but only if the reset succeeded + // and the post-reset result still allows the same deletion type. + auto const typesToApply = typesForResult(result); + if (isTecClaim(result) && !typesToApply.empty()) + { + auto const viewJ = ctx_.registry.get().getJournal("View"); + for (auto const& [type, ids] : deletedObjects) + { + if (ids.empty() || !typesToApply.contains(type)) + continue; + + switch (type) + { + case ltOFFER: + removeUnfundedOffers(view(), ids, viewJ); + break; + case ltNFTOKEN_OFFER: + removeExpiredNFTokenOffers(view(), ids, viewJ); + break; + case ltRIPPLE_STATE: + removeDeletedTrustLines(view(), ids, viewJ); + break; + case ltCREDENTIAL: + removeExpiredCredentials(view(), ids, viewJ); + break; + // LCOV_EXCL_START + default: + UNREACHABLE( + "xrpl::Transactor::processPersistentChanges() : " + "unexpected type"); + break; + // LCOV_EXCL_STOP + } + } + } + + return {result, fee, isTecClaim(result)}; +} + [[nodiscard]] TER Transactor::checkTransactionInvariants(TER result, XRPAmount fee) { @@ -1209,6 +1303,7 @@ Transactor::checkInvariants(TER result, XRPAmount fee) */ return ctx_.checkInvariants(result, fee); } + //------------------------------------------------------------------------------ ApplyResult Transactor::operator()() @@ -1275,108 +1370,7 @@ Transactor::operator()() (result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) || (result == tecEXPIRED) || (isTecClaimHardFail(result, view().flags()))) { - JLOG(j_.trace()) << "reapplying because of " << transToken(result); - - // FIXME: This mechanism for doing work while returning a `tec` is - // awkward and very limiting. A more general purpose approach - // should be used, making it possible to do more useful work - // when transactions fail with a `tec` code. - std::vector removedOffers; - std::vector removedTrustLines; - std::vector removedMPTs; - std::vector expiredNFTokenOffers; - std::vector expiredCredentials; - - bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED)); - bool const doLinesOrMPTs = (result == tecINCOMPLETE); - bool const doNFTokenOffers = (result == tecEXPIRED); - bool const doCredentials = (result == tecEXPIRED); - if (doOffers || doLinesOrMPTs || doNFTokenOffers || doCredentials) - { - ctx_.visit([doOffers, - &removedOffers, - doLinesOrMPTs, - &removedTrustLines, - &removedMPTs, - doNFTokenOffers, - &expiredNFTokenOffers, - doCredentials, - &expiredCredentials]( - uint256 const& index, - bool isDelete, - SLE::const_ref before, - SLE::const_ref after) { - if (isDelete) - { - XRPL_ASSERT( - before && after, - "xrpl::Transactor::operator()::visit : non-null SLE " - "inputs"); - if (doOffers && before && after && (before->getType() == ltOFFER) && - (before->getFieldAmount(sfTakerPays) == after->getFieldAmount(sfTakerPays))) - { - // Removal of offer found or made unfunded - removedOffers.push_back(index); - } - - if (doLinesOrMPTs && before && after) - { - // Removal of obsolete AMM trust line - if (before->getType() == ltRIPPLE_STATE) - { - removedTrustLines.push_back(index); - } - else if (before->getType() == ltMPTOKEN) - { - removedMPTs.push_back(index); - } - } - - if (doNFTokenOffers && before && after && - (before->getType() == ltNFTOKEN_OFFER)) - expiredNFTokenOffers.push_back(index); - - if (doCredentials && before && after && (before->getType() == ltCREDENTIAL)) - expiredCredentials.push_back(index); - } - }); - } - - // Reset the context, potentially adjusting the fee. - { - auto const resetResult = reset(fee); - if (!isTesSuccess(resetResult.first)) - result = resetResult.first; - - fee = resetResult.second; - } - - // If necessary, remove any offers found unfunded during processing - if ((result == tecOVERSIZE) || (result == tecKILLED)) - { - removeUnfundedOffers(view(), removedOffers, ctx_.registry.get().getJournal("View")); - } - - if (result == tecEXPIRED) - { - removeExpiredNFTokenOffers( - view(), expiredNFTokenOffers, ctx_.registry.get().getJournal("View")); - } - - if (result == tecINCOMPLETE) - { - removeDeletedTrustLines( - view(), removedTrustLines, ctx_.registry.get().getJournal("View")); - removeDeletedMPTs(view(), removedMPTs, ctx_.registry.get().getJournal("View")); - } - - if (result == tecEXPIRED) - { - removeExpiredCredentials( - view(), expiredCredentials, ctx_.registry.get().getJournal("View")); - } - - applied = isTecClaim(result); + std::tie(result, fee, applied) = processPersistentChanges(result, fee); } if (applied) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ba8f09c449..cb92b23a4c 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -1120,6 +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(ownerCount(env, buyer) == buyerCount); @@ -1143,6 +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(ownerCount(env, alice) == aliceCount); BEAST_EXPECT(ownerCount(env, buyer) == buyerCount); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 7382f4f090..ea8b0a7c0e 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -797,11 +797,13 @@ public: // The offer expires (it's not removed yet). env.close(); env.require(Owners(bob, 1), offers(bob, 1)); + auto const expiredBobOffer = keylet::offer(bob, env.seq(bob) - 1); // bob creates the offer that will be crossed. env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS)); env.close(); env.require(Owners(bob, 2), offers(bob, 2)); + auto const crossedBobOffer = keylet::offer(bob, env.seq(bob) - 1); env(trust(alice, usd(1000)), Ter(tesSUCCESS)); env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS)); @@ -820,6 +822,8 @@ public: Balance(bob, usd(kNone)), Owners(bob, 1), offers(bob, 1)); + BEAST_EXPECT(!env.current()->exists(expiredBobOffer)); + BEAST_EXPECT(env.current()->exists(crossedBobOffer)); // Order that can be filled env(offer(alice, XRP(500), usd(500)), Txflags(tfFillOrKill), Ter(tesSUCCESS)); @@ -835,6 +839,27 @@ public: offers(bob, 0)); } + // A failed Fill-or-Kill may tentatively consume a funded offer before + // the transaction is reset. That offer must not be treated as an + // unfunded offer cleanup. + { + Env env{*this, features}; + + env.fund(startBalance, gw, alice, bob); + env.close(); + + env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS)); + env.close(); + auto const bobOffer = keylet::offer(bob, env.seq(bob) - 1); + + env(trust(alice, usd(1000)), Ter(tesSUCCESS)); + env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS)); + env(offer(alice, XRP(1000), usd(1000)), Txflags(tfFillOrKill), Ter(tecKILLED)); + + env.require(offers(alice, 0), offers(bob, 1), Balance(alice, usd(1000))); + BEAST_EXPECT(env.current()->exists(bobOffer)); + } + // Immediate or Cancel - cross as much as possible // and add nothing on the books: { diff --git a/src/test/app/SetRegularKey_test.cpp b/src/test/app/SetRegularKey_test.cpp index b5d1af9ef0..b512885606 100644 --- a/src/test/app/SetRegularKey_test.cpp +++ b/src/test/app/SetRegularKey_test.cpp @@ -72,6 +72,27 @@ public: env(regkey(alice, alice), Ter(temBAD_REGKEY)); } + void + testNoAlternativeKey() + { + using namespace test::jtx; + + testcase("Cannot remove last signing method"); + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(10000), alice); + + env(regkey(alice, bob)); + env(fset(alice, asfDisableMaster), Sig(alice)); + + env(regkey(alice, kDisabled), Sig(bob), Ter(tecNO_ALTERNATIVE_KEY)); + + auto const sle = env.le(alice); + BEAST_EXPECT( + sle && sle->isFlag(lsfDisableMaster) && sle->getAccountID(sfRegularKey) == bob.id()); + } + void testPasswordSpent() { @@ -169,6 +190,7 @@ public: { testDisabledMasterKey(); testDisabledRegularKey(); + testNoAlternativeKey(); testPasswordSpent(); testUniversalMask(); testTicketRegularKey(); From ff02269c0dd1707b38ae783fdf9ba05b07037af7 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 22 Jun 2026 18:35:28 -0400 Subject: [PATCH 041/121] refactor: Use dispatch instead of post (#7438) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- src/xrpld/overlay/detail/PeerImp.cpp | 367 ++++++++++++--------------- 1 file changed, 167 insertions(+), 200 deletions(-) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 21e84d6fc7..e1d7d23215 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -70,7 +70,6 @@ #include #include #include -#include #include #include #include @@ -198,78 +197,70 @@ stringIsUInt256Sized(std::string const& pBuffStr) void PeerImp::run() { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::run, shared_from_this())); - return; - } + dispatch(strand_, [self = shared_from_this()]() { + auto parseLedgerHash = [](std::string_view value) -> std::optional { + if (uint256 ret; ret.parseHex(value)) + return ret; - auto parseLedgerHash = [](std::string_view value) -> std::optional { - if (uint256 ret; ret.parseHex(value)) - return ret; + if (auto const s = base64Decode(value); s.size() == uint256::size()) + return uint256::fromRaw(s); - if (auto const s = base64Decode(value); s.size() == uint256::size()) - return uint256::fromRaw(s); + return std::nullopt; + }; - return std::nullopt; - }; + std::optional closed; + std::optional previous; - std::optional closed; - std::optional previous; + if (auto const iter = self->headers_.find("Closed-Ledger"); iter != self->headers_.end()) + { + closed = parseLedgerHash(iter->value()); - if (auto const iter = headers_.find("Closed-Ledger"); iter != headers_.end()) - { - closed = parseLedgerHash(iter->value()); + if (!closed) + self->fail("Malformed handshake data (1)"); + } - if (!closed) - fail("Malformed handshake data (1)"); - } + if (auto const iter = self->headers_.find("Previous-Ledger"); iter != self->headers_.end()) + { + previous = parseLedgerHash(iter->value()); - if (auto const iter = headers_.find("Previous-Ledger"); iter != headers_.end()) - { - previous = parseLedgerHash(iter->value()); + if (!previous) + self->fail("Malformed handshake data (2)"); + } - if (!previous) - fail("Malformed handshake data (2)"); - } + if (previous && !closed) + self->fail("Malformed handshake data (3)"); - if (previous && !closed) - fail("Malformed handshake data (3)"); + { + std::scoped_lock const sl(self->recentLock_); + if (closed) + self->closedLedgerHash_ = *closed; + if (previous) + self->previousLedgerHash_ = *previous; + } - { - std::scoped_lock const sl(recentLock_); - if (closed) - closedLedgerHash_ = *closed; - if (previous) - previousLedgerHash_ = *previous; - } + if (self->inbound_) + { + self->doAccept(); + } + else + { + self->doProtocolStart(); + } - if (inbound_) - { - doAccept(); - } - else - { - doProtocolStart(); - } - - // Anything else that needs to be done with the connection should be - // done in doProtocolStart + // Anything else that needs to be done with the connection should be + // done in doProtocolStart + }); } void PeerImp::stop() { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::stop, shared_from_this())); - return; - } + dispatch(strand_, [self = shared_from_this()]() { + if (!self->socket_.is_open()) + return; - if (!socket_.is_open()) - return; - - close(); + self->close(); + }); } //------------------------------------------------------------------------------ @@ -277,126 +268,111 @@ PeerImp::stop() void PeerImp::send(std::shared_ptr const& m) { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::send, shared_from_this(), m)); - return; - } - if (gracefulClose_) - return; - if (detaching_) - return; - if (!socket_.is_open()) - return; + dispatch(strand_, [self = shared_from_this(), m]() { + if (self->gracefulClose_) + return; + if (self->detaching_) + return; + if (!self->socket_.is_open()) + return; - auto validator = m->getValidatorKey(); - if (validator && !squelch_.expireSquelch(*validator)) - { - overlay_.reportOutboundTraffic( - TrafficCount::Category::SquelchSuppressed, - static_cast(m->getBuffer(compressionEnabled_).size())); - return; - } + auto validator = m->getValidatorKey(); + if (validator && !self->squelch_.expireSquelch(*validator)) + { + self->overlay_.reportOutboundTraffic( + TrafficCount::Category::SquelchSuppressed, + static_cast(m->getBuffer(self->compressionEnabled_).size())); + return; + } - // report categorized outgoing traffic - overlay_.reportOutboundTraffic( - safeCast(m->getCategory()), - static_cast(m->getBuffer(compressionEnabled_).size())); + // report categorized outgoing traffic + self->overlay_.reportOutboundTraffic( + safeCast(m->getCategory()), + static_cast(m->getBuffer(self->compressionEnabled_).size())); - // report total outgoing traffic - overlay_.reportOutboundTraffic( - TrafficCount::Category::Total, static_cast(m->getBuffer(compressionEnabled_).size())); + // report total outgoing traffic + self->overlay_.reportOutboundTraffic( + TrafficCount::Category::Total, + static_cast(m->getBuffer(self->compressionEnabled_).size())); - auto sendqSize = sendQueue_.size(); + auto sendqSize = self->sendQueue_.size(); - if (sendqSize < Tuning::kTargetSendQueue) - { - // To detect a peer that does not read from their - // side of the connection, we expect a peer to have - // a small senq periodically - largeSendq_ = 0; - } - else if (auto sink = journal_.debug(); sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0) - { - std::string const n = name(); - sink << n << " sendq: " << sendqSize; - } + if (sendqSize < Tuning::kTargetSendQueue) + { + // To detect a peer that does not read from their + // side of the connection, we expect a peer to have + // a small sendq periodically + self->largeSendq_ = 0; + } + else if ( + auto sink = self->journal_.debug(); + sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0) + { + std::string const n = self->name(); + sink << n << " sendq: " << sendqSize; + } - sendQueue_.push(m); + self->sendQueue_.push(m); - if (sendqSize != 0) - return; + if (sendqSize != 0) + return; - boost::asio::async_write( - stream_, - boost::asio::buffer(sendQueue_.front()->getBuffer(compressionEnabled_)), - bind_executor( - strand_, - std::bind( - &PeerImp::onWriteMessage, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + boost::asio::async_write( + 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))); + }); } void PeerImp::sendTxQueue() { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::sendTxQueue, shared_from_this())); - return; - } - - if (!txQueue_.empty()) - { - protocol::TMHaveTransactions ht; - std::ranges::for_each( - txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); }); - JLOG(pJournal_.trace()) << "sendTxQueue " << txQueue_.size(); - txQueue_.clear(); - send(std::make_shared(ht, protocol::mtHAVE_TRANSACTIONS)); - } + dispatch(strand_, [self = shared_from_this()]() { + if (!self->txQueue_.empty()) + { + protocol::TMHaveTransactions ht; + std::ranges::for_each( + self->txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); }); + JLOG(self->pJournal_.trace()) << "sendTxQueue " << self->txQueue_.size(); + self->txQueue_.clear(); + self->send(std::make_shared(ht, protocol::mtHAVE_TRANSACTIONS)); + } + }); } void PeerImp::addTxQueue(uint256 const& hash) { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::addTxQueue, shared_from_this(), hash)); - return; - } + dispatch(strand_, [self = shared_from_this(), hash]() { + if (self->txQueue_.size() == reduce_relay::kMaxTxQueueSize) + { + JLOG(self->pJournal_.warn()) << "addTxQueue exceeds the cap"; + self->sendTxQueue(); + } - if (txQueue_.size() == reduce_relay::kMaxTxQueueSize) - { - JLOG(pJournal_.warn()) << "addTxQueue exceeds the cap"; - sendTxQueue(); - } - - txQueue_.insert(hash); - JLOG(pJournal_.trace()) << "addTxQueue " << txQueue_.size(); + self->txQueue_.insert(hash); + JLOG(self->pJournal_.trace()) << "addTxQueue " << self->txQueue_.size(); + }); } void PeerImp::removeTxQueue(uint256 const& hash) { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::removeTxQueue, shared_from_this(), hash)); - return; - } - - auto removed = txQueue_.erase(hash); - JLOG(pJournal_.trace()) << "removeTxQueue " << removed; + dispatch(strand_, [self = shared_from_this(), hash]() { + auto removed = self->txQueue_.erase(hash); + JLOG(self->pJournal_.trace()) << "removeTxQueue " << removed; + }); } void PeerImp::charge(Resource::Charge const& fee, std::string const& context) { - dispatch(strand_, [this, self = shared_from_this(), fee, context]() { - if ((usage_.charge(fee, context) == Resource::Disposition::Drop) && - usage_.disconnect(pJournal_)) + dispatch(strand_, [self = shared_from_this(), fee, context]() { + if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) && + self->usage_.disconnect(self->pJournal_)) { // Idempotent: only the first worker to observe Drop counts the // metric and posts fail(). Without the guard, several queued @@ -405,11 +381,11 @@ PeerImp::charge(Resource::Charge const& fee, std::string const& context) // shutdowns. fail(std::string const&) self-posts to strand_ // when invoked off-strand. bool expected = false; - if (chargeDisconnectFired_.compare_exchange_strong( + if (self->chargeDisconnectFired_.compare_exchange_strong( expected, true, std::memory_order_acq_rel)) { - overlay_.incPeerDisconnectCharges(); - fail("charge: Resources"); + self->overlay_.incPeerDisconnectCharges(); + self->fail("charge: Resources"); } } }); @@ -640,20 +616,14 @@ PeerImp::close() void PeerImp::fail(std::string const& reason) { - if (!strand_.running_in_this_thread()) - { - post( - strand_, - std::bind( - (void (Peer::*)(std::string const&))&PeerImp::fail, shared_from_this(), reason)); - return; - } - if (journal_.active(beast::Severity::Warning) && socket_.is_open()) - { - std::string const n = name(); - JLOG(journal_.warn()) << n << " failed: " << reason; - } - close(); + dispatch(strand_, [self = shared_from_this(), reason]() { + if (self->journal_.active(beast::Severity::Warning) && self->socket_.is_open()) + { + std::string const n = self->name(); + JLOG(self->journal_.warn()) << n << " failed: " << reason; + } + self->close(); + }); } void @@ -2752,45 +2722,42 @@ PeerImp::onMessage(std::shared_ptr const& m) void PeerImp::onMessage(std::shared_ptr const& m) { - using on_message_fn = void (PeerImp::*)(std::shared_ptr const&); - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind((on_message_fn)&PeerImp::onMessage, shared_from_this(), m)); - return; - } + dispatch(strand_, [self = shared_from_this(), m]() { + if (!m->has_validatorpubkey()) + { + self->fee_.update(Resource::kFeeInvalidData, "squelch no pubkey"); + return; + } + auto validator = m->validatorpubkey(); + auto const slice{makeSlice(validator)}; + if (!publicKeyType(slice)) + { + self->fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey"); + return; + } + PublicKey const key(slice); - if (!m->has_validatorpubkey()) - { - fee_.update(Resource::kFeeInvalidData, "squelch no pubkey"); - return; - } - auto validator = m->validatorpubkey(); - auto const slice{makeSlice(validator)}; - if (!publicKeyType(slice)) - { - fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey"); - return; - } - PublicKey const key(slice); + // Ignore the squelch for validator's own messages. + if (key == self->app_.getValidationPublicKey()) + { + JLOG(self->pJournal_.debug()) + << "onMessage: TMSquelch discarding validator's squelch " << slice; + return; + } - // Ignore the squelch for validator's own messages. - if (key == app_.getValidationPublicKey()) - { - JLOG(pJournal_.debug()) << "onMessage: TMSquelch discarding validator's squelch " << slice; - return; - } + std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0; + if (!m->squelch()) + { + self->squelch_.removeSquelch(key); + } + else if (!self->squelch_.addSquelch(key, std::chrono::seconds{duration})) + { + self->fee_.update(Resource::kFeeInvalidData, "squelch duration"); + } - std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0; - if (!m->squelch()) - { - squelch_.removeSquelch(key); - } - else if (!squelch_.addSquelch(key, std::chrono::seconds{duration})) - { - fee_.update(Resource::kFeeInvalidData, "squelch duration"); - } - - JLOG(pJournal_.debug()) << "onMessage: TMSquelch " << slice << " " << id() << " " << duration; + JLOG(self->pJournal_.debug()) + << "onMessage: TMSquelch " << slice << " " << self->id() << " " << duration; + }); } //-------------------------------------------------------------------------- From 0b22050b5e33e5a46e0a294124f547cd5fd6da49 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Tue, 23 Jun 2026 20:25:38 +0100 Subject: [PATCH 042/121] ci: Update workflows and conan to use VS2026 and grpc 1.81.0 (#7550) Co-authored-by: Ayaz Salikhov --- .github/scripts/strategy-matrix/windows.json | 2 +- .github/workflows/reusable-build-test-config.yml | 4 ++-- conan.lock | 12 ++++++------ conanfile.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index e25f9ad131..370e9f5bc7 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -1,6 +1,6 @@ { "platform": "windows/amd64", - "runner": ["self-hosted", "Windows", "devbox"], + "runner": ["self-hosted", "Windows", "dev-box-windows-2026"], "configs": [ { "build_type": "Release" }, { diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 3e6464aaba..fe441dba6e 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -82,7 +82,7 @@ jobs: name: ${{ inputs.config_name }} runs-on: ${{ fromJSON(inputs.runs_on) }} container: ${{ inputs.image != '' && inputs.image || null }} - timeout-minutes: ${{ inputs.sanitizers != '' && 360 || 90 }} + timeout-minutes: ${{ inputs.sanitizers != '' && 360 || 180 }} env: # Use a namespace to keep the objects separate for each configuration. CCACHE_NAMESPACE: ${{ inputs.config_name }} @@ -163,7 +163,7 @@ jobs: CMAKE_ARGS: ${{ inputs.cmake_args }} run: | cmake \ - -G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \ + -G '${{ runner.os == 'Windows' && 'Visual Studio 18 2026' || 'Ninja' }}' \ -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ ${CMAKE_ARGS} \ diff --git a/conan.lock b/conan.lock index e2eb8d871a..d80a6d0c57 100644 --- a/conan.lock +++ b/conan.lock @@ -1,9 +1,9 @@ { "version": "0.5", "requires": [ - "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1778091116.056", "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987", - "sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1776096494.149", + "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", @@ -15,19 +15,19 @@ "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", - "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1776147552.838", + "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1778091117.848", "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228", "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152", - "grpc/1.78.1#b1a9e74b145cc471bed4dc64dc6eb2c1%1774467387.342", + "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%1778050991.9", + "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1778091165.282", "abseil/20250127.0#bb0baf1f362bc4a725a24eddd419b8f7%1774365460.196" ], "build_requires": [ - "zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503", + "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", diff --git a/conanfile.py b/conanfile.py index 2cf5aefbc2..5b78dc22e3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -28,7 +28,7 @@ class Xrpl(ConanFile): requires = [ "ed25519/2015.03", - "grpc/1.78.1", + "grpc/1.81.0", "libarchive/3.8.7", "nudb/2.0.9", "openssl/3.6.2", From 5a2c82f699f1d5036f572d771e7be597ba80f896 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Tue, 23 Jun 2026 15:55:23 -0400 Subject: [PATCH 043/121] fix: Reject delegate permission to pseudo accounts (#7597) --- src/libxrpl/tx/transactors/delegate/DelegateSet.cpp | 6 +++++- src/test/app/Delegate_test.cpp | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index 82fe88aa9f..32a51555b1 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -52,9 +52,13 @@ DelegateSet::preclaim(PreclaimContext const& ctx) if (!ctx.view.exists(keylet::account(ctx.tx[sfAccount]))) return terNO_ACCOUNT; // LCOV_EXCL_LINE - if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))) + auto const sleAuthorize = ctx.view.read(keylet::account(ctx.tx[sfAuthorize])); + if (!sleAuthorize) return tecNO_TARGET; + if (isPseudoAccount(sleAuthorize)) + return tecNO_PERMISSION; + // Deleting the delegate object is invalid if it doesn’t exist. if (ctx.tx.getFieldArray(sfPermissions).empty() && !ctx.view.exists(keylet::delegate(ctx.tx[sfAccount], ctx.tx[sfAuthorize]))) diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 20668a42bf..1516219e46 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -235,6 +235,19 @@ class Delegate_test : public beast::unit_test::Suite env(delegate::set(gw, Account("unknown"), {"Payment"}), Ter(tecNO_TARGET)); } + // Delegating to a pseudo-account is not allowed, should return tecNO_PERMISSION + { + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = gw, .asset = xrpIssue()}); + env(tx); + env.close(); + + auto const sleVault = env.le(keylet); + BEAST_EXPECT(sleVault); + Account const vaultPseudo{"vault", sleVault->at(sfAccount)}; + env(delegate::set(gw, vaultPseudo, {"Payment"}), Ter(tecNO_PERMISSION)); + } + // non-delegable transaction { env(delegate::set(gw, alice, {"SetRegularKey"}), Ter(temMALFORMED)); From 6341e752002f90dc87ed49650cb704eac5f62660 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Wed, 24 Jun 2026 13:15:11 +0100 Subject: [PATCH 044/121] 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 045/121] 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 046/121] 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 047/121] 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 048/121] 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 049/121] 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 050/121] 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 051/121] 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 052/121] 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 053/121] 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 054/121] 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 055/121] 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 056/121] 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 057/121] 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 058/121] 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 059/121] 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 060/121] 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 061/121] 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 062/121] 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 063/121] 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 064/121] 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 065/121] 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 066/121] 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 067/121] 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 068/121] 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 069/121] 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 070/121] 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 071/121] 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 072/121] 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 073/121] 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 074/121] 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 075/121] 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 076/121] 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 077/121] 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 078/121] 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 079/121] 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 080/121] 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 081/121] 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 082/121] 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 083/121] 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 084/121] 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 085/121] 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 086/121] 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 087/121] 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 088/121] 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