From 0711a7b493c63d261cb7fcb02b22c9de744d8ff7 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 25 Jun 2026 23:06:04 +0100 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 11/15] 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 12/15] 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 13/15] 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 14/15] 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 15/15] 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) {