diff --git a/.clang-tidy b/.clang-tidy index e09d326916..e8e2ca7ac9 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -153,6 +153,7 @@ Checks: "-*, readability-use-std-min-max " # --- +# bugprone-narrowing-conversions, # this will break a lot of code but we should enable it in the future because it can eliminate a lot of bugs # readability-inconsistent-declaration-parameter-name, # In this codebase this check will break a lot of arg names # readability-static-accessed-through-instance, # this check is probably unnecessary. It makes the code less readable # --- diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 12176ec0d4..547c1b3539 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -14,7 +14,6 @@ libxrpl.ledger > xrpl.json libxrpl.ledger > xrpl.ledger libxrpl.ledger > xrpl.nodestore libxrpl.ledger > xrpl.protocol -libxrpl.ledger > xrpl.server libxrpl.ledger > xrpl.shamap libxrpl.net > xrpl.basics libxrpl.net > xrpl.net @@ -221,7 +220,6 @@ xrpl.core > xrpl.protocol xrpl.json > xrpl.basics xrpl.ledger > xrpl.basics xrpl.ledger > xrpl.protocol -xrpl.ledger > xrpl.server xrpl.ledger > xrpl.shamap xrpl.net > xrpl.basics xrpl.nodestore > xrpl.basics diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index 6353567f27..a269cb25d4 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -20,8 +20,6 @@ _SANITIZER_SUFFIX: dict[str, str] = { def get_cmake_args(build_type: str, extra_args: str) -> str: """Get the full list of CMake arguments for a config.""" args = _BASE_CMAKE_ARGS.copy() - if build_type == "Release": - args.append("-Dassert=ON") if extra_args: args.extend(extra_args.split()) return " ".join(args) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 4f45216cda..a9b85b766a 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -1,5 +1,5 @@ { - "image_tag": "sha-63ffdc3", + "image_tag": "sha-fe4c8ae", "configs": { "ubuntu": [ { @@ -68,7 +68,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], - "image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-63ffdc3" + "image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745" } ], @@ -77,7 +77,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], - "image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-63ffdc3" + "image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745" } ] } diff --git a/.github/workflows/build-nix-images.yml b/.github/workflows/build-nix-images.yml index 24f069902d..54911ef6e0 100644 --- a/.github/workflows/build-nix-images.yml +++ b/.github/workflows/build-nix-images.yml @@ -9,12 +9,20 @@ on: - "flake.nix" - "flake.lock" - "nix/**" + - "!nix/docker/README.md" + - "!nix/devshell.nix" + - "bin/check-tools.sh" + - "bin/install-sanitizer-libs.sh" pull_request: paths: - ".github/workflows/build-nix-images.yml" - "flake.nix" - "flake.lock" - "nix/**" + - "!nix/docker/README.md" + - "!nix/devshell.nix" + - "bin/check-tools.sh" + - "bin/install-sanitizer-libs.sh" workflow_dispatch: concurrency: @@ -46,9 +54,9 @@ jobs: base_image: debian:bookworm - name: rhel base_image: registry.access.redhat.com/ubi9/ubi:latest - uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07 + uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c with: - image_name: ghcr.io/xrplf/xrpld/nix-${{ matrix.distro.name }} + image_name: xrpld/nix-${{ matrix.distro.name }} dockerfile: nix/docker/Dockerfile base_image: ${{ matrix.distro.base_image }} - push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }} + push: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/build-packaging-images.yml b/.github/workflows/build-packaging-images.yml index d6dabb0f95..3633847ef3 100644 --- a/.github/workflows/build-packaging-images.yml +++ b/.github/workflows/build-packaging-images.yml @@ -38,9 +38,9 @@ jobs: base_image: debian:bookworm - name: rhel base_image: registry.access.redhat.com/ubi9/ubi:latest - uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@c1b480188519e0cad040e6aa70db1cbc5a797e07 + uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@ee03d31bcc4501d7599dc1b1ecd7a34af582ad1c with: - image_name: ghcr.io/xrplf/xrpld/packaging-${{ matrix.distro.name }} + image_name: xrpld/packaging-${{ matrix.distro.name }} dockerfile: package/Dockerfile base_image: ${{ matrix.distro.base_image }} - push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }} + push: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/check-pr-description.yml b/.github/workflows/check-pr-description.yml index a60b83738a..744449f216 100644 --- a/.github/workflows/check-pr-description.yml +++ b/.github/workflows/check-pr-description.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Write PR body to file env: diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 4b2edeb93d..0cc9b375a7 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Determine changed files # This step checks whether any files have changed that should # cause the next jobs to run. We do it this way rather than diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index aecf0c2a8b..0363534af5 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -14,7 +14,7 @@ on: jobs: # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: - uses: XRPLF/actions/.github/workflows/pre-commit.yml@312aaab296060ff89d7f798dcab59f019bea6e02 + uses: XRPLF/actions/.github/workflows/pre-commit.yml@e06d4138c9ec8dceeb7c818645faa38087ea9e3d with: runs_on: ubuntu-latest container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index bcf5968384..cc7b6b6e7e 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -41,10 +41,10 @@ env: jobs: build: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 28d317e4dd..3e6464aaba 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -110,7 +110,7 @@ jobs: uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 @@ -121,6 +121,11 @@ jobs: if: ${{ inputs.ccache_enabled && runner.debug == '1' }} run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}" + - name: Check tools + env: + CHECK_TOOLS_SKIP_CLONE: "1" + run: ./bin/check-tools.sh + - name: Print build environment uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574 diff --git a/.github/workflows/reusable-check-levelization.yml b/.github/workflows/reusable-check-levelization.yml index 813c0e1e36..88c95ac3ba 100644 --- a/.github/workflows/reusable-check-levelization.yml +++ b/.github/workflows/reusable-check-levelization.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check levelization run: python .github/scripts/levelization/generate.py - name: Check for differences diff --git a/.github/workflows/reusable-check-rename.yml b/.github/workflows/reusable-check-rename.yml index 5002cc7f40..9a91e98ee3 100644 --- a/.github/workflows/reusable-check-rename.yml +++ b/.github/workflows/reusable-check-rename.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check definitions run: .github/scripts/rename/definitions.sh . - name: Check copyright notices diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 9f10711b6f..e99ef574bf 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -20,29 +20,32 @@ env: BUILD_DIR: build BUILD_TYPE: Debug # Debug so that ASSERTS and such participate in clang-tidy check - OUTPUT_FILE: clang-tidy-output.txt - DIFF_FILE: clang-tidy-git-diff.txt - ISSUE_FILE: clang-tidy-issue.md + OUTPUT_FILE: /tmp/clang-tidy-output.txt + FILTERED_OUTPUT_FILE: /tmp/clang-tidy-filtered-output.txt + DIFF_FILE: /tmp/clang-tidy-git-diff.txt + ISSUE_FILE: /tmp/clang-tidy-issue.md + + COMPILER: clang jobs: determine-files: if: ${{ inputs.check_only_changed }} permissions: contents: read - uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@312aaab296060ff89d7f798dcab59f019bea6e02 + uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@c7045074aafe9fb92fa537aa4446f81fbfc17e8b run-clang-tidy: name: Run clang tidy needs: [determine-files] if: ${{ always() && !cancelled() && (!inputs.check_only_changed || needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.clang_tidy_config_changed == 'true') }} runs-on: ["self-hosted", "Linux", "X64", "heavy"] - container: "ghcr.io/xrplf/xrpld/nix-debian:sha-63ffdc3" + container: "ghcr.io/xrplf/xrpld/nix-debian:sha-fe4c8ae" permissions: contents: read issues: write steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 @@ -59,7 +62,7 @@ jobs: - name: Set compiler environment uses: ./.github/actions/set-compiler-env with: - compiler: clang + compiler: ${{ env.COMPILER }} - name: Setup Conan uses: ./.github/actions/setup-conan @@ -150,21 +153,21 @@ jobs: run: | if [ -f "${OUTPUT_FILE}" ]; then # Extract lines containing 'error:', 'warning:', or 'note:' - grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >filtered-output.txt || true + grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true # If filtered output is empty, use original (might be a different error format) - if [ ! -s filtered-output.txt ]; then - cp "${OUTPUT_FILE}" filtered-output.txt + if [ ! -s "${FILTERED_OUTPUT_FILE}" ]; then + cp "${OUTPUT_FILE}" "${FILTERED_OUTPUT_FILE}" fi # Truncate if too large - head -c 60000 filtered-output.txt >>"${ISSUE_FILE}" - if [ "$(wc -c >"${ISSUE_FILE}" + if [ "$(wc -c <"${FILTERED_OUTPUT_FILE}")" -gt 60000 ]; then echo "" >>"${ISSUE_FILE}" echo "... (output truncated, see artifacts for full output)" >>"${ISSUE_FILE}" fi - rm filtered-output.txt + rm "${FILTERED_OUTPUT_FILE}" else echo "No output file found" >>"${ISSUE_FILE}" fi diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 0e3f657006..eed4bfc4a3 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -27,7 +27,7 @@ jobs: matrix: ${{ steps.generate.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -45,7 +45,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | .github/actions/generate-version @@ -69,7 +69,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Download pre-built binary uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index 4518a8ffef..c1a1c1a78b 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -23,7 +23,7 @@ jobs: matrix: ${{ steps.generate.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index 1c90fb0e72..a389e98771 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -40,10 +40,10 @@ defaults: jobs: upload: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-63ffdc3 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Generate build version number id: version diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 7ca9d13007..5d3712cf9e 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -65,7 +65,7 @@ jobs: uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 diff --git a/BUILD.md b/BUILD.md index 662ba0d33d..2ac24f2c5d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,26 +1,57 @@ -| :warning: **WARNING** :warning: | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md). | +| :warning: **WARNING** :warning: | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).

These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. | -> These instructions also assume a basic familiarity with Conan and CMake. -> If you are unfamiliar with Conan, you can read our -> [crash course](./docs/build/conan.md) or the official [Getting Started][3] -> walkthrough. +## Minimum Requirements -## Branches +See [System Requirements](https://xrpl.org/system-requirements.html). -For a stable release, choose the `master` branch or one of the [tagged -releases](https://github.com/XRPLF/rippled/releases). +Building xrpld generally requires Git, Python, Conan, CMake, and a C++ +compiler. + +- [Python](https://www.python.org/downloads/) +- [Conan](https://conan.io/downloads.html) +- [CMake](https://cmake.org/download/) + +You can verify that the required tools are installed and runnable with: ```bash -git checkout master +./bin/check-tools.sh ``` -For the latest release candidate, choose the `release` branch. +`xrpld` is written in the C++23 dialect. The [tested compiler versions][cpp23-support] are: -```bash -git checkout release -``` +| Compiler | Version | +| ----------- | --------------- | +| GCC | 15.2 | +| Clang | 22 | +| Apple Clang | 17 | +| MSVC | 19.44[^windows] | + +## Operating Systems + +Please see the [environment setup guide](./docs/build/environment.md) for detailed instructions for all platforms. + +### Linux + +The Ubuntu Linux distribution has received the highest level of quality +assurance, testing, and support. We also support Red Hat and use Debian +internally. +Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them. + +### macOS + +Many `xrpld` engineers use macOS for development. + +### Windows + +Windows is used by some engineers for development only. + +[^windows]: Windows is not recommended for production use. + +## Steps + +### Branches For the latest set of untested features, or to contribute, choose the `develop` branch. @@ -29,55 +60,15 @@ branch. git checkout develop ``` -## Minimum Requirements +For a release candidate, choose the relevant release branch, e.g. +`release/3.2.x`. -See [System Requirements](https://xrpl.org/system-requirements.html). +```bash +git checkout release/3.2.x +``` -Building xrpld generally requires git, Python, Conan, CMake, and a C++ -compiler. Some guidance on setting up such a [C++ development environment can be -found here](./docs/build/environment.md). - -- [Python 3.11](https://www.python.org/downloads/), or higher -- [Conan 2.17](https://conan.io/downloads.html)[^1], or higher -- [CMake 3.22](https://cmake.org/download/), or higher - -[^1]: - It is possible to build with Conan 1.60+, but the instructions are - significantly different, which is why we are not recommending it. - -`xrpld` is written in the C++23 dialect and includes the `` header. -The [tested compiler versions][2] are: - -| Compiler | Version | -| ----------- | --------- | -| GCC | 15 | -| Clang | 22 | -| Apple Clang | 17 | -| MSVC | 19.44[^3] | - -### Linux - -The Ubuntu Linux distribution has received the highest level of quality -assurance, testing, and support. We also support Red Hat and use Debian -internally. - -Here are [sample instructions for setting up a C++ development environment on -Linux](./docs/build/environment.md#linux). - -### Mac - -Many xrpld engineers use macOS for development. - -Here are [sample instructions for setting up a C++ development environment on -macOS](./docs/build/environment.md#macos). - -### Windows - -Windows is used by some engineers for development only. - -[^3]: Windows is not recommended for production use. - -## Steps +For a stable release, choose one of the [tagged +releases](https://github.com/XRPLF/rippled/releases). ### Set Up Conan @@ -86,18 +77,11 @@ Conan, CMake, and a C++ compiler, you may need to set up your Conan profile. These instructions assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, then please read [this crash course](./docs/build/conan.md) or the official -[Getting Started][3] walkthrough. +[Getting Started][conan-getting-started] walkthrough. -#### Conan lockfile +#### Profiles -To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html), -which has to be updated every time dependencies change. - -Please see the [instructions on how to regenerate the lockfile](conan/lockfile/README.md). - -#### Default profile - -We recommend that you import the provided `conan/profiles/default` profile: +We recommend that you install our Conan profiles: ```bash conan config install conan/profiles/ -tf $(conan config home)/profiles/ @@ -109,222 +93,15 @@ You can check your Conan profile by running: conan profile show ``` -#### Custom profile +If the default profile is not suitable for your environment, you can create a custom profile and pass it to Conan. +More information on customizing Conan can be found in the [Advanced Conan configuration](./docs/build/advanced_conan.md). -If the default profile does not work for you and you do not yet have a Conan -profile, you can create one by running: +#### Add xrplf remote + +Run the following command to add the `xrplf` remote, which hosts some of our dependencies: ```bash -conan profile detect -``` - -You may need to make changes to the profile to suit your environment. You can -refer to the provided `conan/profiles/default` profile for inspiration, and you -may also need to apply the required [tweaks](#conan-profile-tweaks) to this -default profile. - -### Patched recipes - -Occasionally, we need patched recipes or recipes not present in Conan Center. -We maintain a fork of the Conan Center Index -[here](https://github.com/XRPLF/conan-center-index/) containing the modified and newly added recipes. - -To ensure our patched recipes are used, you must add our Conan remote at a -higher index than the default Conan Center remote, so it is consulted first. You -can do this by running: - -```bash -conan remote add --index 0 xrplf https://conan.ripplex.io -``` - -Alternatively, you can pull our recipes from the repository and export them locally: - -```bash -# Define which recipes to export. -recipes=('abseil' 'ed25519' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi') - -# Selectively check out the recipes from our CCI fork. -cd external -mkdir -p conan-center-index -cd conan-center-index -git init -git remote add origin git@github.com:XRPLF/conan-center-index.git -git sparse-checkout init -for recipe in "${recipes[@]}"; do - echo "Checking out recipe '${recipe}'..." - git sparse-checkout add recipes/${recipe} -done -git fetch origin master -git checkout master - -./export_all.sh -cd ../../ -``` - -In the case we switch to a newer version of a dependency that still requires a -patch or add a new dependency, it will be necessary for you to pull in the changes and re-export the -updated dependencies with the newer version. However, if we switch to a newer -version that no longer requires a patch, no action is required on your part, as -the new recipe will be automatically pulled from the official Conan Center. - -> [!NOTE] -> You might need to add `--lockfile=""` to your `conan install` command -> to avoid automatic use of the existing `conan.lock` file when you run -> `conan export` manually on your machine -> -> This is not recommended though, as you might end up using different revisions of recipes. - -### Conan profile tweaks - -#### Missing compiler version - -If you see an error similar to the following after running `conan profile show`: - -```text -ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value. -Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1', -'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15', -'15.0', '16', '16.0'] -Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting" -``` - -you need to add your compiler to the list of compiler versions in -`$(conan config home)/settings_user.yml`, by adding the required version number(s) -to the `version` array specific for your compiler. For example: - -```yaml -compiler: - apple-clang: - version: ["17.0"] -``` - -#### Multiple compilers - -If you have multiple compilers installed, make sure to select the one to use in -your default Conan configuration **before** running `conan profile detect`, by -setting the `CC` and `CXX` environment variables. - -For example, if you are running MacOS and have [homebrew -LLVM@18](https://formulae.brew.sh/formula/llvm@18), and want to use it as a -compiler in the new Conan profile: - -```bash -export CC=$(brew --prefix llvm@18)/bin/clang -export CXX=$(brew --prefix llvm@18)/bin/clang++ -conan profile detect -``` - -You should also explicitly set the path to the compiler in the profile file, -which helps to avoid errors when `CC` and/or `CXX` are set and disagree with the -selected Conan profile. For example: - -```text -[conf] -tools.build:compiler_executables={'c':'/usr/bin/gcc','cpp':'/usr/bin/g++'} -``` - -#### Multiple profiles - -You can manage multiple Conan profiles in the directory -`$(conan config home)/profiles`, for example renaming `default` to a different -name and then creating a new `default` profile for a different compiler. - -#### Select language - -The default profile created by Conan will typically select different C++ dialect -than C++23 used by this project. You should set `23` in the profile line -starting with `compiler.cppstd=`. For example: - -```bash -sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)/profiles/default -``` - -#### Select standard library in Linux - -**Linux** developers will commonly have a default Conan [profile][] that -compiles with GCC and links with libstdc++. If you are linking with libstdc++ -(see profile setting `compiler.libcxx`), then you will need to choose the -`libstdc++11` ABI: - -```bash -sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan config home)/profiles/default -``` - -#### Select architecture and runtime in Windows - -**Windows** developers may need to use the x64 native build tools. An easy way -to do that is to run the shortcut "x64 Native Tools Command Prompt" for the -version of Visual Studio that you have installed. - -Windows developers must also build `xrpld` and its dependencies for the x64 -architecture: - -```bash -sed -i.bak -e 's|^arch=.*$|arch=x86_64|' $(conan config home)/profiles/default -``` - -**Windows** developers also must select static runtime: - -```bash -sed -i.bak -e 's|^compiler\.runtime=.*$|compiler.runtime=static|' $(conan config home)/profiles/default -``` - -#### Clang workaround for grpc - -If your compiler is clang, version 19 or later, or apple-clang, version 17 or -later, you may encounter a compilation error while building the `grpc` -dependency: - -```text -In file included from .../lib/promise/try_seq.h:26: -.../lib/promise/detail/basic_seq.h:499:38: error: a template argument list is expected after a name prefixed by the template keyword [-Wmissing-template-arg-list-after-template-kw] - 499 | Traits::template CallSeqFactory(f_, *cur_, std::move(arg))); - | ^ -``` - -The workaround for this error is to add two lines to profile: - -```text -[conf] -tools.build:cxxflags=['-Wno-missing-template-arg-list-after-template-kw'] -``` - -#### Workaround for gcc 12 - -If your compiler is gcc, version 12, and you have enabled `werr` option, you may -encounter a compilation error such as: - -```text -/usr/include/c++/12/bits/char_traits.h:435:56: error: 'void* __builtin_memcpy(void*, const void*, long unsigned int)' accessing 9223372036854775810 or more bytes at offsets [2, 9223372036854775807] and 1 may overlap up to 9223372036854775813 bytes at offset -3 [-Werror=restrict] - 435 | return static_cast(__builtin_memcpy(__s1, __s2, __n)); - | ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~ -cc1plus: all warnings being treated as errors -``` - -The workaround for this error is to add two lines to your profile: - -```text -[conf] -tools.build:cxxflags=['-Wno-restrict'] -``` - -#### Workaround for clang 16 - -If your compiler is clang, version 16, you may encounter compilation error such -as: - -```text -In file included from .../boost/beast/websocket/stream.hpp:2857: -.../boost/beast/websocket/impl/read.hpp:695:17: error: call to 'async_teardown' is ambiguous - async_teardown(impl.role, impl.stream(), - ^~~~~~~~~~~~~~ -``` - -The workaround for this error is to add two lines to your profile: - -```text -[conf] -tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS'] +conan remote add --index 0 --force xrplf https://conan.ripplex.io ``` ### Set Up Ccache @@ -333,14 +110,7 @@ To speed up repeated compilations, we recommend that you install [ccache](https://ccache.dev), a tool that wraps your compiler so that it can cache build objects locally. -#### Linux - -You can install it using the package manager, e.g. `sudo apt install ccache` -(Ubuntu) or `sudo dnf install ccache` (RHEL). - -#### macOS - -You can install it using Homebrew, i.e. `brew install ccache`. +On Linux and macOS, `ccache` is included in the [Nix development shell](./docs/build/nix.md). #### Windows @@ -549,7 +319,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details. | Option | Default Value | Description | | ---------- | ------------- | -------------------------------------------------------------- | -| `assert` | OFF | Enable assertions. | +| `assert` | OFF | Force enabling assertions. | | `coverage` | OFF | Prepare the coverage report. | | `tests` | OFF | Build tests. | | `unity` | OFF | Configure a unity build. | @@ -557,7 +327,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details. | `werr` | OFF | Treat compilation warnings as errors | | `wextra` | OFF | Enable additional compilation warnings | -[Unity builds][5] may be faster for the first build (at the cost of much more +[Unity builds][unity-build] may be faster for the first build (at the cost of much more memory) since they concatenate sources into fewer translation units. Non-unity builds may be faster for incremental builds, and can be helpful for detecting `#include` omissions. @@ -583,14 +353,14 @@ After any updates or changes to dependencies, you may need to do the following: conan remove '*' ``` -3. Re-run [conan export](#patched-recipes) if needed. -4. [Regenerate lockfile](#conan-lockfile). +3. Re-run [conan export](./docs/build/advanced_conan.md#patched-recipes) if needed. +4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile). 5. Re-run [conan install](#build-and-test). #### ERROR: Package not resolved If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, -please add `xrplf` remote or re-run `conan export` for [patched recipes](#patched-recipes). +please [add `xrplf` remote](#add-xrplf-remote) or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes). ### `protobuf/port_def.inc` file not found @@ -610,28 +380,9 @@ For example, if you want to build Debug: 1. For conan install, pass `--settings build_type=Debug` 2. For cmake, pass `-DCMAKE_BUILD_TYPE=Debug` -## Add a Dependency - -If you want to experiment with a new package, follow these steps: - -1. Search for the package on [Conan Center](https://conan.io/center/). -2. Modify [`conanfile.py`](./conanfile.py): - - Add a version of the package to the `requires` property. - - Change any default options for the package by adding them to the - `default_options` property (with syntax `'$package:$option': $value`). -3. Modify [`CMakeLists.txt`](./CMakeLists.txt): - - Add a call to `find_package($package REQUIRED)`. - - Link a library from the package to the target `xrpl_libs` - (search for the existing call to `target_link_libraries(xrpl_libs INTERFACE ...)`). -4. Start coding! Don't forget to include whatever headers you need from the package. - -[1]: https://github.com/conan-io/conan-center-index/issues/13168 -[2]: https://en.cppreference.com/w/cpp/compiler_support/20 -[3]: https://docs.conan.io/en/latest/getting_started.html -[5]: https://en.wikipedia.org/wiki/Unity_build -[6]: https://github.com/boostorg/beast/issues/2648 -[7]: https://github.com/boostorg/beast/issues/2661 +[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23 +[conan-getting-started]: https://docs.conan.io/en/latest/getting_started.html +[unity-build]: https://en.wikipedia.org/wiki/Unity_build [gcovr]: https://gcovr.com/en/stable/getting-started.html [python-pip]: https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/ [build_type]: https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html -[profile]: https://docs.conan.io/en/latest/reference/profiles.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25dd7ac059..fc93223925 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,9 @@ The following branches exist in the main project repository: - `develop`: The latest set of unreleased features, and the most common starting point for contributions. -- `release`: The latest beta release or release candidate. -- `master`: The latest stable release. -- `gh-pages`: The documentation for this project, built by Doxygen. +- `release/*` (e.g. `release/3.2.x`): Release branches, one per release line, + holding the latest release candidate, or stable release for that line. + Stable releases are published as [tagged releases](https://github.com/XRPLF/rippled/releases). The tip of each branch must be signed. In order for GitHub to sign a squashed commit that it builds from your pull request, GitHub must know @@ -130,11 +130,9 @@ tl;dr ## Pull requests In general, pull requests use `develop` as the base branch. -The exceptions are -- Fixes and improvements to a release candidate use `release` as the - base. -- Hotfixes use `master` as the base. +The exceptions are fixes, improvements, and hotfixes for an existing release, +which use that release's branch (e.g. `release/3.2.x`) as the base. If your changes are not quite ready, but you want to make it easily available for preliminary examination or review, you can create a "Draft" pull request. @@ -216,7 +214,7 @@ coherent rather than a set of _thou shalt not_ commandments. ## Formatting -All code must conform to `clang-format` version 21, +All code must conform to `clang-format` version 22, according to the settings in [`.clang-format`](./.clang-format), unless the result would be unreasonably difficult to read or maintain. To demarcate lines that should be left as-is, surround them with comments like @@ -261,7 +259,7 @@ This ensures that configuration changes don't introduce new warnings across the ### Installing clang-tidy -See the [environment setup guide](./docs/build/environment.md#clang-tidy) for platform-specific installation instructions. +See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy. ### Running clang-tidy locally diff --git a/bin/check-tools.sh b/bin/check-tools.sh new file mode 100755 index 0000000000..15b16b6fc8 --- /dev/null +++ b/bin/check-tools.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# +# check-tools.sh — verify the xrpld development tooling is present and runnable. +# +# Works on Linux, macOS, and Windows (Git Bash / MSYS). For every expected tool +# it runs a version probe, collecting anything that is missing or fails to run, +# and prints a summary at the end (exiting non-zero if anything is missing). +# +# The tool set is platform-aware: +# - Linux: the full Nix CI environment (see nix/packages.nix, nix/ci-env.nix), +# with GCC, Clang and the sanitizer/coverage tooling. This script is +# run during the Nix Docker image build (nix/docker/Dockerfile), so +# the Linux list is kept in sync with that environment. +# - macOS: the same tooling, minus GCC/g++/gcov/mold +# - Windows: the core build tools only (CMake, Conan, Git, Python). +# MSVC is expected to be provided separately and is not checked here. +# +# Some tools (clang-format, doxygen, gcovr, gh, git-cliff, gpg, pre-commit, +# run-clang-tidy) are present in our Linux CI images and in local development +# setups, but not in the macOS CI environment. They are checked everywhere +# except when running in CI on macOS. +# +# Environment variables: +# CI if set, skip the tools above when on macOS. +# CHECK_TOOLS_SKIP_CLONE if set, skip the git-over-HTTPS connectivity check. + +set -uo pipefail + +missing=() +checked=0 + +# check [probe-command...] +# Runs the probe (default: " --version") quietly. Records as +# missing if the command is not found or exits non-zero. +check() { + local name="$1" + shift + local -a probe=("$@") + if [ "${#probe[@]}" -eq 0 ]; then + probe=("${name}" --version) + fi + + echo "Checking ${name}..." + checked=$((checked + 1)) + if "${probe[@]}" | head -n 1; then + printf ' [ ok ] %s\n' "${name}" + else + printf ' [MISS] %s\n' "${name}" + missing+=("${name}") + fi +} + +case "$(uname -s)" in + Linux*) os=linux ;; + Darwin*) os=macos ;; + MINGW* | MSYS* | CYGWIN*) os=windows ;; + *) + echo "Unknown OS: $(uname -s)" >&2 + exit 1 + ;; +esac + +echo "Detected OS: ${os} ($(uname -s) $(uname -m))" +echo +echo "Core build tools:" +check cmake +check conan +check git +if [ "${os}" = "windows" ]; then + check python python --version +else + check python3 +fi + +# The full development toolchain. Available from Nix on Linux and macOS; on +# Windows these are typically not installed, so they are skipped. +if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then + echo + echo "Development tooling:" + check ccache + check clang + check clang++ + check ClangBuildAnalyzer + check curl + check file + check less + check make + check netstat which netstat + check ninja + check perl + check pkg-config + check vim + + # These tools are present in our Linux CI images and in local development + # setups, but not in the macOS CI environment. So check them everywhere + # except when running in CI on macOS. + if [ "${os}" = "linux" ] || [ -z "${CI:-}" ]; then + check clang-format + check doxygen + check gcovr + check gh + check git-cliff + check gpg + # pre-commit, or its alternative implementation prek + check pre-commit sh -c 'pre-commit --version || prek --version' + check run-clang-tidy run-clang-tidy --help + fi +fi + +# GCC is the default compiler on Linux. macOS uses the system Apple Clang +# instead, so GCC/g++/gcov are not expected there. +if [ "${os}" = "linux" ]; then + echo + echo "GCC toolchain:" + check gcc + check g++ + check gcov + + echo + echo "Mold:" + check mold +fi + +if [ "${os}" = "windows" ]; then + echo + echo "Note: on Windows the C++ compiler is MSVC, which is provided" + echo " separately (e.g. via Visual Studio) and is not checked here." +fi + +# A simple test to verify that git can clone a repository over HTTPS +# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up. +if [ -n "${CHECK_TOOLS_SKIP_CLONE:-}" ]; then + echo + echo "Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set)." +else + echo + echo "Connectivity check:" + checked=$((checked + 1)) + tmp_clone="$(mktemp -d)" + if git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions" >/dev/null 2>&1; then + printf ' [ ok ] git clone over HTTPS\n' + else + printf ' [MISS] git clone over HTTPS\n' + missing+=("git-https-clone") + fi + rm -rf "${tmp_clone}" +fi + +echo +if [ "${#missing[@]}" -eq 0 ]; then + echo "All ${checked} checked tools are present and runnable." +else + echo "Missing or non-functional tools (${#missing[@]} of ${checked}):" >&2 + for tool in "${missing[@]}"; do + echo " - ${tool}" >&2 + done + exit 1 +fi diff --git a/nix/docker/install-sanitizer-libs.sh b/bin/install-sanitizer-libs.sh similarity index 100% rename from nix/docker/install-sanitizer-libs.sh rename to bin/install-sanitizer-libs.sh diff --git a/cspell.config.yaml b/cspell.config.yaml index b6ed5215b9..386e1367c3 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -109,6 +109,7 @@ words: - enabled - enablerepo - endmacro + - envrc - exceptioned - EXPECT_STREQ - Falco @@ -318,6 +319,7 @@ words: - unserviced - unshareable - unshares + - unsponsored - unsquelch - unsquelched - unsquelching diff --git a/docs/build/advanced_conan.md b/docs/build/advanced_conan.md new file mode 100644 index 0000000000..aae17e385a --- /dev/null +++ b/docs/build/advanced_conan.md @@ -0,0 +1,193 @@ +# Advanced Conan configuration + +This document provides advanced instructions for setting up and configuring Conan for `xrpld` development: custom profiles, the lockfile, patched recipes, and profile tweaks. + +## Custom profile + +If the default profile does not work for you and you do not yet have a Conan +profile, you can create one by running: + +```bash +conan profile detect +``` + +You may need to make changes to the profile to suit your environment. You can +refer to the provided `conan/profiles/default` profile for inspiration, and you +may also need to apply the required [tweaks](#conan-profile-tweaks) to this +default profile. + +## Conan lockfile + +To achieve reproducible dependencies, we use a [Conan lockfile](https://docs.conan.io/2/tutorial/versioning/lockfiles.html), +which has to be updated every time dependencies change. + +Please see the [instructions on how to regenerate the lockfile](../../conan/lockfile/README.md). + +## Patched recipes + +Occasionally, we need patched recipes or recipes not present in Conan Center. +We maintain a fork of the Conan Center Index +[here](https://github.com/XRPLF/conan-center-index/) containing the modified and newly added recipes. + +To ensure our patched recipes are used, you must add our Conan remote at a +higher index than the default Conan Center remote, so it is consulted first. You +can do this by running: + +```bash +conan remote add --index 0 --force xrplf https://conan.ripplex.io +``` + +Alternatively, you can pull our recipes from the repository and export them locally: + +```bash +# Define which recipes to export. +recipes=('abseil' 'ed25519' 'mpt-crypto' 'openssl' 'secp256k1' 'snappy' 'soci' 'wasm-xrplf' 'wasmi') + +# Selectively check out the recipes from our CCI fork. +cd external +mkdir -p conan-center-index +cd conan-center-index +git init +git remote add origin git@github.com:XRPLF/conan-center-index.git +git sparse-checkout init +for recipe in "${recipes[@]}"; do + echo "Checking out recipe '${recipe}'..." + git sparse-checkout add recipes/${recipe} +done +git fetch origin master +git checkout master + +./export_all.sh +cd ../../ +``` + +In the case we switch to a newer version of a dependency that still requires a +patch or add a new dependency, it will be necessary for you to pull in the changes and re-export the +updated dependencies with the newer version. However, if we switch to a newer +version that no longer requires a patch, no action is required on your part, as +the new recipe will be automatically pulled from the official Conan Center. + +> [!NOTE] +> You might need to add `--lockfile=""` to your `conan install` command +> to avoid automatic use of the existing `conan.lock` file when you run +> `conan export` manually on your machine +> +> This is not recommended though, as you might end up using different revisions of recipes. + +## Conan profile tweaks + +### Missing compiler version + +If you see an error similar to the following after running `conan profile show`: + +```text +ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value. +Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1', +'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15', +'15.0', '16', '16.0'] +Read "http://docs.conan.io/2/knowledge/faq.html#error-invalid-setting" +``` + +you need to create `$(conan config home)/settings_user.yml` file if it doesn't exist and add the required version number(s) +to the `version` array specific for your compiler. For example: + +```yaml +compiler: + apple-clang: + version: ["17.0"] +``` + +### Multiple compilers + +If you have multiple compilers installed, make sure to select the one to use in +your default Conan configuration **before** running `conan profile detect`, by +setting the `CC` and `CXX` environment variables. + +For example, if you are running MacOS and have [homebrew +LLVM@18](https://formulae.brew.sh/formula/llvm@18), and want to use it as a +compiler in the new Conan profile: + +```bash +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +conan profile detect +``` + +You should also explicitly set the path to the compiler in the profile file, +which helps to avoid errors when `CC` and/or `CXX` are set and disagree with the +selected Conan profile. For example: + +```text +[conf] +tools.build:compiler_executables={'c':'/usr/bin/gcc','cpp':'/usr/bin/g++'} +``` + +### Multiple profiles + +You can manage multiple Conan profiles in the directory +`$(conan config home)/profiles`, for example renaming `default` to a different +name and then creating a new `default` profile for a different compiler. + +### Select language + +The default profile created by Conan will typically select different C++ dialect +than C++23 used by this project. You should set `23` in the profile line +starting with `compiler.cppstd=`. For example: + +```bash +sed -i.bak -e 's|^compiler\.cppstd=.*$|compiler.cppstd=23|' $(conan config home)/profiles/default +``` + +### Select standard library in Linux + +**Linux** developers will commonly have a default Conan [profile][] that +compiles with GCC and links with libstdc++. If you are linking with libstdc++ +(see profile setting `compiler.libcxx`), then you will need to choose the +`libstdc++11` ABI: + +```bash +sed -i.bak -e 's|^compiler\.libcxx=.*$|compiler.libcxx=libstdc++11|' $(conan config home)/profiles/default +``` + +### Select architecture and runtime in Windows + +**Windows** developers may need to use the x64 native build tools. An easy way +to do that is to run the shortcut "x64 Native Tools Command Prompt" for the +version of Visual Studio that you have installed. + +Windows developers must also build `xrpld` and its dependencies for the x64 +architecture: + +```bash +sed -i.bak -e 's|^arch=.*$|arch=x86_64|' $(conan config home)/profiles/default +``` + +**Windows** developers also must select static runtime: + +```bash +sed -i.bak -e 's|^compiler\.runtime=.*$|compiler.runtime=static|' $(conan config home)/profiles/default +``` + +## Add a Dependency + +If you want to experiment with a new package, follow these steps: + +1. Search for the package on [Conan Center](https://conan.io/center/). +2. Modify [`conanfile.py`](../../conanfile.py): + - Add a version of the package to the `requires` property. + - Change any default options for the package by adding them to the + `default_options` property (with syntax `'$package:$option': $value`). +3. Regenerate the [Conan lockfile](../../conan/lockfile/README.md) so the new + dependency is captured: + + ```bash + ./conan/lockfile/regenerate.sh + ``` + +4. Modify [`CMakeLists.txt`](../../CMakeLists.txt): + - Add a call to `find_package($package REQUIRED)`. + - Link a library from the package to the target `xrpl_libs` + (search for the existing call to `target_link_libraries(xrpl_libs INTERFACE ...)`). +5. Start coding! Don't forget to include whatever headers you need from the package. + +[profile]: https://docs.conan.io/2/reference/config_files/profiles.html diff --git a/docs/build/conan.md b/docs/build/conan.md index 9dcd2c8f1c..22c25a0bf9 100644 --- a/docs/build/conan.md +++ b/docs/build/conan.md @@ -115,7 +115,7 @@ By default, Conan will use the profile named "default". [find_package]: https://cmake.org/cmake/help/latest/command/find_package.html [pcf]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-configuration-file [prefix_path]: https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html -[profile]: https://docs.conan.io/en/latest/reference/profiles.html +[profile]: https://docs.conan.io/2/reference/config_files/profiles.html [pvf]: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-version-file [runtime]: https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_RUNTIME_LIBRARY.html [search]: https://cmake.org/cmake/help/latest/command/find_package.html#search-procedure diff --git a/docs/build/environment.md b/docs/build/environment.md index fb1ebde8bc..2cca608567 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -1,69 +1,73 @@ Our [build instructions][BUILD.md] assume you have a C++ development environment complete with Git, Python, Conan, CMake, and a C++ compiler. -This document exists to help readers set one up on any of the Big Three -platforms: Linux, macOS, or Windows. - -As an alternative to system packages, the Nix development shell can be used to provide a development environment. See [using nix development shell](./nix.md) for more details. +This document explains how to set one up. [BUILD.md]: ../../BUILD.md -## Linux +## Tested compiler versions -Package ecosystems vary across Linux distributions, -so there is no one set of instructions that will work for every Linux user. -The instructions below are written for Debian 12 (Bookworm). +`xrpld` is built in the **C++23** dialect by default. +Make sure your toolchain is recent enough — the compiler versions currently tested in CI are: -``` -export GCC_RELEASE=12 -sudo apt update -sudo apt install --yes gcc-${GCC_RELEASE} g++-${GCC_RELEASE} python3-pip \ - python-is-python3 python3-venv python3-dev curl wget ca-certificates \ - git build-essential cmake ninja-build libc6-dev -sudo pip install --break-system-packages conan +| Compiler | Version | +| ----------- | ------- | +| GCC | 15.2 | +| Clang | 22 | +| Apple Clang | 17 | +| MSVC | 19.44 | -sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-${GCC_RELEASE} 999 -sudo update-alternatives --install \ - /usr/bin/gcc gcc /usr/bin/gcc-${GCC_RELEASE} 100 \ - --slave /usr/bin/g++ g++ /usr/bin/g++-${GCC_RELEASE} \ - --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-${GCC_RELEASE} \ - --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-${GCC_RELEASE} \ - --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-${GCC_RELEASE} \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-${GCC_RELEASE} \ - --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-${GCC_RELEASE} \ - --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-${GCC_RELEASE} \ - --slave /usr/bin/lto-dump lto-dump /usr/bin/lto-dump-${GCC_RELEASE} -sudo update-alternatives --auto cc -sudo update-alternatives --auto gcc +LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22. + +Older compilers may fail to build the latest `develop` code: the codebase now +relies on C++23 features and has been adjusted for `clang-tidy`. +If the latest code doesn't build for you, update your build toolchain first. + +## Linux and macOS + +The **recommended way** to get a development environment on Linux and macOS is +the Nix development shell. It provides the exact tooling used in CI — `git`, +`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else — +with a single command and without installing anything system-wide: + +```bash +nix --experimental-features 'nix-command flakes' develop ``` -If you use different Linux distribution, hope the instruction above can guide -you in the right direction. We try to maintain compatibility with all recent -compiler releases, so if you use a rolling distribution like e.g. Arch or CentOS -then there is a chance that everything will "just work". +On **Linux**, Nix also provides the compiler (GCC). On **macOS**, the shell uses +your **system-wide Apple Clang** as the compiler, so you still need to manage +its version (see below). -## macOS +See [Using the Nix development shell](./nix.md) for installation and usage +details, including how to select a different compiler. -Open a Terminal and enter the below command to bring up a dialog to install -the command line developer tools. -Once it is finished, this command should return a version greater than the -minimum required (see [BUILD.md][]). +> [!NOTE] +> Using Nix is not mandatory. Any custom environment (Homebrew packages or +> anything else) will continue to work, but then it is up to you to keep it in +> sync with the environment used in CI. Nix unifies the development environment +> for everyone and synchronizes updates, which is why we recommend it. -``` +### macOS: managing the Apple Clang version + +Because the Nix shell uses the system-wide Apple Clang on macOS, the compiler +version is whatever your installed Xcode (or Command Line Tools) provides. The +following command should return a version greater than or equal to the +[minimum required](#tested-compiler-versions): + +```bash clang --version ``` -### Install Xcode Specific Version (Optional) - -If you develop other applications using XCode you might be consistently updating to the newest version of Apple Clang. -This will likely cause issues building xrpld. You may want to install a specific version of Xcode: +If you develop other applications using Xcode, you might be consistently +updating to the newest version of Apple Clang, which will likely cause issues +building xrpld. You may want to install and pin a specific version of Xcode: 1. **Download Xcode** - Visit [Apple Developer Downloads](https://developer.apple.com/download/more/) - Sign in with your Apple Developer account - - Search for an Xcode version that includes **Apple Clang (Expected Version)** + - Search for an Xcode version that includes the expected Apple Clang version - Download the `.xip` file -2. **Install and Configure Xcode** +2. **Install and configure Xcode** ```bash # Extract the .xip file and rename for version management @@ -79,62 +83,28 @@ This will likely cause issues building xrpld. You may want to install a specific export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer ``` -The command line developer tools should include Git too: +## Windows -``` -git --version -``` +Nix is not available on Windows, so the required tools have to be installed +manually: -Install [Homebrew][], -use it to install [pyenv][], -use it to install Python, -and use it to install Conan: +- [Visual Studio 2022](https://visualstudio.microsoft.com/) with the + **"Desktop development with C++"** workload — this provides MSVC and the + "x64 Native Tools Command Prompt". +- [Git for Windows](https://git-scm.com/download/win) +- [Python 3.11](https://www.python.org/downloads/), or higher +- [Conan 2.17](https://conan.io/downloads.html), or higher +- [CMake 3.22](https://cmake.org/download/), or higher -[Homebrew]: https://brew.sh/ -[pyenv]: https://github.com/pyenv/pyenv - -``` -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -brew update -brew install xz -brew install pyenv -pyenv install 3.11 -pyenv global 3.11 -eval "$(pyenv init -)" -pip install 'conan' -``` - -Install CMake with Homebrew too: - -``` -brew install cmake -``` +> [!NOTE] +> Windows is used for development only and is not recommended for production. ## Clang-tidy -Clang-tidy is required to run static analysis checks locally (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). -It is not required to build the project. Currently this project uses clang-tidy version 21. +`clang-tidy` is required to run static analysis checks locally (see +[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the +project. This project currently uses `clang-tidy` version 22. -### Linux - -LLVM 21 is not available in the default Debian 12 (Bookworm) repositories. -Install it using the official LLVM apt installer: - -``` -wget https://apt.llvm.org/llvm.sh -chmod +x llvm.sh -sudo ./llvm.sh 21 -sudo apt install --yes clang-tidy-21 -``` - -Then use `run-clang-tidy-21` when running clang-tidy locally. - -### macOS - -Install LLVM 21 via Homebrew: - -``` -brew install llvm@21 -``` - -Then use `run-clang-tidy` from the LLVM 21 Homebrew prefix when running clang-tidy locally. +On Linux and macOS, the [Nix development shell](./nix.md) provides `clang-tidy` +22 out of the box — run it via `run-clang-tidy`. No separate installation is +needed. diff --git a/docs/build/nix.md b/docs/build/nix.md index 33bb3711d0..2ae483aefe 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -2,9 +2,12 @@ This guide explains how to use Nix to set up a reproducible development environment for xrpld. Using Nix eliminates the need to manually install utilities and ensures consistent tooling across different machines. +**The Nix development shell is the recommended way to develop xrpld.** It unifies the development environment for everyone and synchronizes updates: the same tooling and compiler versions are used both here and in CI. Any custom environment (Homebrew packages or anything else) will continue to work, but then it is up to you to keep it in sync with the environment used in CI. + ## Benefits of Using Nix - **Reproducible environment**: Everyone gets the same versions of tools and compilers +- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment - **No system pollution**: Dependencies are isolated and don't affect your system packages - **Multiple compiler versions**: Easily switch between different GCC and Clang versions - **Quick setup**: Get started with a single command @@ -28,11 +31,22 @@ This will: - Download and set up all required development tools (CMake, Ninja, Conan, etc.) - Configure the appropriate compiler for your platform: - - **macOS**: Apple Clang (default system compiler) - - **Linux**: GCC 15 + - **Linux**: GCC 15.2 (provided by Nix) + - **macOS**: Apple Clang (your system compiler) The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster. +### Platform notes + +- **Linux**: `nix develop` gives you a shell with all the tooling necessary to + develop xrpld and with GCC 15.2 (also provided by Nix). There are no caveats. +- **macOS**: `nix develop` gives you a full environment too. The compiler is + your system-wide Apple Clang, while every other tool — including Conan — is + provided by Nix. Conan has no binary in the Nix cache for macOS, so it is + built from source the first time you enter the shell, which makes the initial + setup slower (this is handled automatically; see + [`nix/devshell.nix`](../../nix/devshell.nix)). + > [!TIP] > To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`: > @@ -51,7 +65,7 @@ The first time you run this command, it will take a few minutes to download and A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`. Use `nix flake show` to see all the available development shells. -Use `nix develop .#no_compiler` to use the compiler from your system. +Use `nix develop .#no-compiler` to use the compiler from your system. ### Example Usage @@ -68,12 +82,28 @@ nix develop ### Using a different shell -`nix develop` opens bash by default. If you want to use another shell this could be done by adding `-c` flag. For example: +`nix develop` opens bash by default. To use another shell, pass it with the `-c` flag — this works with any shell, e.g. `zsh` or `fish`: ```bash +# Use zsh nix develop -c zsh + +# Use fish +nix develop -c fish + +# Use your login shell +nix develop -c "$SHELL" ``` +> [!WARNING] +> Your shell's interactive startup files (e.g. `config.fish`, `.zshrc`) may prepend other directories — most commonly Homebrew — to `$PATH`, which can shadow the tools provided by the Nix shell. After entering, verify that tools resolve into the Nix store: +> +> ```bash +> command -v cmake # should print a /nix/store/... path +> ``` +> +> If it doesn't, either adjust your shell configuration so it doesn't override `$PATH`, or use [direnv](#automatic-activation-with-direnv) (below), which loads the environment _after_ your shell config and so takes precedence regardless of the shell you use. + ## Building xrpld with Nix Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). @@ -82,6 +112,8 @@ Once inside the Nix development shell, follow the standard [build instructions]( [direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory. +This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`. To use it, install direnv for your shell, then add an `.envrc` containing `use flake` at the repository root and run `direnv allow`. + ## Conan and Prebuilt Packages Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source: @@ -93,3 +125,8 @@ conan install .. --output-folder . --build '*' --settings build_type=Release ## Updating `flake.lock` file To update `flake.lock` to the latest revision use `nix flake update` command. + +## Troubleshooting + +See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues, +such as `nix develop` failing inside Git worktrees. diff --git a/docs/build/nix_troubleshooting.md b/docs/build/nix_troubleshooting.md new file mode 100644 index 0000000000..ae5cb8059a --- /dev/null +++ b/docs/build/nix_troubleshooting.md @@ -0,0 +1,61 @@ +# Troubleshooting Nix problems + +Common issues encountered when using the [Nix development shell](./nix.md), and +how to resolve them. + +## Git worktrees + +If `nix develop` fails with an error like: + +``` +error: + … while fetching the input 'git+file:///path/to/rippled' + + error: opening Git repository "/path/to/rippled": unsupported extension name extensions.relativeworktrees (libgit2 error code = 6) +``` + +then your Nix is linked against a libgit2 older than **1.9.4**. Git 2.48+ writes +the `extensions.relativeWorktrees` config entry when a worktree is created with +relative paths (`git worktree add --relative-paths`, or with +`worktree.useRelativePaths=true`), and older libgit2 versions refuse to open a +repository that uses it. Nix uses libgit2 to read the flake, so evaluation +fails. + +> [!IMPORTANT] +> This entry is written to the **shared** repository config, so once any +> relative worktree exists, `nix develop` fails in the main checkout too — not +> just inside the worktree. + +### Workarounds + +These work today, with any Nix version: + +- bypass libgit2 with a `path:` flakeref: `nix develop "path:$PWD"` + (note: this copies the working tree to the store and ignores `.gitignore`); or +- create worktrees with absolute paths (omit `--relative-paths`); or +- clear the extension if you don't need relative worktrees: + `git config --unset extensions.relativeWorktrees`. + +### Permanent fix + +The fix is in [libgit2 1.9.4](https://github.com/libgit2/libgit2/releases/tag/v1.9.4), +so the real solution is a Nix that links against libgit2 `1.9.4` or newer. Check +which version yours links against: + +```bash +nix-store -qR "$(readlink -f "$(command -v nix)")" | grep libgit2 +``` + +> [!WARNING] +> `nix upgrade-nix` does **not** help yet. It installs the build from the +> official [`nix-fallback-paths`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/tools/nix-fallback-paths.nix), +> which is still linked against libgit2 `1.9.2` — there is no new upstream Nix +> release with the fix. (On some systems that build is even the exact store path +> you already have, making the upgrade a no-op.) + +nixpkgs has already rebuilt Nix against the fixed libgit2 (e.g. `nix-2.34.7+1`), +so the cleanest path is to reinstall Nix using your usual installation method +once it picks up that rebuild, then re-run the `grep libgit2` check above to +confirm it reports `1.9.4` or newer. + +Until then, prefer the workarounds above. diff --git a/include/xrpl/basics/rocksdb.h b/include/xrpl/basics/rocksdb.h deleted file mode 100644 index 3d468b0f1b..0000000000 --- a/include/xrpl/basics/rocksdb.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#if XRPL_ROCKSDB_AVAILABLE -// #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#endif diff --git a/include/xrpl/basics/sanitizers.h b/include/xrpl/basics/sanitizers.h index b954952848..7937344b53 100644 --- a/include/xrpl/basics/sanitizers.h +++ b/include/xrpl/basics/sanitizers.h @@ -4,7 +4,7 @@ /* ASAN flags some false positives with sudden jumps in control flow, like exceptions, or when encountering coroutine stack switches. This macro can be used to disable ASAN - intrumentation for specific functions. + instrumentation for specific functions. */ #if defined(__GNUC__) || defined(__clang__) #define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress"))) diff --git a/include/xrpl/ledger/BookListeners.h b/include/xrpl/ledger/BookListeners.h deleted file mode 100644 index 3b96aca680..0000000000 --- a/include/xrpl/ledger/BookListeners.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -namespace xrpl { - -/** Listen to public/subscribe messages from a book. */ -class BookListeners -{ -public: - using pointer = std::shared_ptr; - - BookListeners() = default; - - /** Add a new subscription for this book - */ - void - addSubscriber(InfoSub::ref sub); - - /** Stop publishing to a subscriber - */ - void - removeSubscriber(std::uint64_t sub); - - /** Publish a transaction to subscribers - - Publish a transaction to clients subscribed to changes on this book. - Uses havePublished to prevent sending duplicate transactions to clients - that have subscribed to multiple books. - - @param jvObj JSON transaction data to publish - @param havePublished InfoSub sequence numbers that have already - published this transaction. - - */ - void - publish(MultiApiJson const& jvObj, hash_set& havePublished); - -private: - std::recursive_mutex lock_; - - hash_map listeners_; -}; - -} // namespace xrpl diff --git a/include/xrpl/ledger/OrderBookDB.h b/include/xrpl/ledger/OrderBookDB.h index a0aee58e2a..a44183900c 100644 --- a/include/xrpl/ledger/OrderBookDB.h +++ b/include/xrpl/ledger/OrderBookDB.h @@ -1,11 +1,11 @@ #pragma once +#include +#include #include -#include #include #include #include -#include #include #include @@ -77,34 +77,24 @@ public: */ virtual bool isBookToXRP(Asset const& asset, std::optional const& domain = std::nullopt) = 0; - - /** - * Process a transaction for order book tracking. - * @param ledger The ledger the transaction was applied to - * @param alTx The transaction to process - * @param jvObj The JSON object of the transaction - */ - virtual void - processTxn( - std::shared_ptr const& ledger, - AcceptedLedgerTx const& alTx, - MultiApiJson const& jvObj) = 0; - - /** - * Get the book listeners for a book. - * @param book The book to get the listeners for - * @return The book listeners for the book - */ - virtual BookListeners::pointer - getBookListeners(Book const&) = 0; - - /** - * Create a new book listeners for a book. - * @param book The book to create the listeners for - * @return The new book listeners for the book - */ - virtual BookListeners::pointer - makeBookListeners(Book const&) = 0; }; +/** Extract the set of books affected by a transaction. + * + * Walks the transaction's metadata nodes and collects every order book + * whose offers were created, modified, or deleted. Used by NetworkOPs to + * fan transaction notifications out to book subscribers. + * + * @param alTx The accepted ledger transaction to inspect. + * @param j Journal used to log per-node parsing failures. Inspecting an + * offer node can throw if a required field is missing; in that + * case the bad node is skipped and a warn-level message is + * emitted via @p j. Other affected books in the same transaction + * are still returned. + * @return The set of books whose offers were created, modified, or + * deleted. May be empty for non-offer transactions. + */ +hash_set +affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j); + } // namespace xrpl diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index d21e50e7cb..de8bb9d3f7 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -37,6 +37,8 @@ reduceOffer(auto const& amount) enum class IsDeposit : bool { No = false, Yes = true }; +inline Number const kAMMInvariantRelativeTolerance{1, -11}; + /** Calculate LP Tokens given AMM pool reserves. * @param asset1 AMM one side of the pool reserve * @param asset2 AMM another side of the pool reserve @@ -738,6 +740,30 @@ ammPoolHolds( AuthHandling authHandling, beast::Journal const j); +/** Check AMM pool product invariant after an AMM operation that changes LP tokens + * (deposit/withdraw/clawback) from an already calculated pool product mean. + * Returns tecPRECISION_LOSS if poolProductMean < newLPTokenBalance beyond the + * invariant tolerance, + * tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal). + */ +TER +checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance); + +/** Check AMM pool product invariant after an AMM operation that changes LP tokens + * (deposit/withdraw/clawback). + * Returns tecPRECISION_LOSS if sqrt(asset1 * asset2) < newLPTokenBalance beyond + * the invariant tolerance, + * tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal). + */ +TER +checkAMMPrecisionLoss( + ReadView const& view, + AccountID const& ammAccountID, + Asset const& asset1, + Asset const& asset2, + STAmount const& newLPTokenBalance, + beast::Journal const j); + /** Get AMM pool and LP token balances. If both optIssue are * provided then they are used as the AMM token pair issues. * Otherwise the missing issues are fetched from ammSle. diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index e19fe23393..403626d06f 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -21,46 +21,92 @@ namespace xrpl { [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); -// Calculate liquid XRP balance for an account. -// This function may be used to calculate the amount of XRP that -// the holder is able to freely spend. It subtracts reserve requirements. -// -// ownerCountAdj adjusts the owner count in case the caller calculates -// before ledger entries are added or removed. Positive to add, negative -// to subtract. -// -// @param ownerCountAdj positive to add to count, negative to reduce count. +/** Calculate liquid XRP balance for an account. + * + * This function may be used to calculate the amount of XRP that + * the holder is able to freely spend. It subtracts reserve requirements. + * + * ownerCountAdj adjusts the owner count in case the caller calculates + * before ledger entries are added or removed. Positive to add, negative + * to subtract. + * + * @param view The ledger view to read from + * @param id The account ID to check + * @param ownerCountAdj Positive to add to count, negative to reduce count + * @param j Journal for logging + * @return The liquid XRP amount available to the account + */ [[nodiscard]] XRPAmount xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j); /** Returns the account reserve, in drops. - Actual owner count can be adjusted by delta in ownerCountAdj - The reserve is calculated as - (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) * - increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base" -*/ + * + * Actual owner count can be adjusted by delta in ownerCountAdj + * Actual reserve count can be adjusted by delta in accountCountAdj + * The reserve is calculated as: + * (ownerCount + "sponsoring object count" - "sponsored object count" + additionalOwnerCount) * + * increment + (1 if not sponsored account + sponsoringAccountCount) * "reserve base" + * + * @param view The ledger view to read from + * @param sle The ledger entry for the account + * @param j Journal for logging + * @param ownerCountAdj Adjustment to the owner count (default: 0) + * @param accountCountAdj Adjustment to the account count (default: 0) + * @return The account reserve amount in drops + */ [[nodiscard]] XRPAmount accountReserve( ReadView const& view, SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj = 0, - std::int32_t reserveCountAdj = 0); + std::int32_t accountCountAdj = 0); +/** Convenience overload that accepts AccountID instead of SLE. + * + * @param view The ledger view to read from + * @param id The account ID + * @param j Journal for logging + * @param ownerCountAdj Adjustment to the owner count (default: 0) + * @param accountCountAdj Adjustment to the account count (default: 0) + * @return The account reserve amount in drops + */ [[nodiscard]] inline XRPAmount accountReserve( ReadView const& view, AccountID const& id, beast::Journal j, std::int32_t ownerCountAdj = 0, - std::int32_t reserveCountAdj = 0) + std::int32_t accountCountAdj = 0) { - return accountReserve(view, view.read(keylet::account(id)), j, ownerCountAdj, reserveCountAdj); + return accountReserve(view, view.read(keylet::account(id)), j, ownerCountAdj, accountCountAdj); } +/** @brief Return the hypothetical reserve required by an account with the provided counters. + * + * @param view The ledger view to read from + * @param ownerCount Number of objects for which the account will be responsible. + * @param accountCount Number of accounts for which the account will be responsible. + * Defaults to 1, as normally every account is responsible for its own reserve. + * Can be 0 if the account is sponsored. + * Can be greater than 1 if the account is sponsoring other accounts. + * @return The hypothetical reserve amount + */ XRPAmount -baseAccountReserve(ReadView const& view, std::int32_t ownerCount); +baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t accountCount = 1); +/** Check if an account has insufficient reserve. + * + * @param view The ledger view to read from + * @param tx The transaction being processed + * @param accSle The account's ledger entry + * @param accBalance The account's balance + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param ownerCountAdj Adjustment to the owner count + * @param accountCountAdj Adjustment to the account count (default: 0) + * @param j Journal for logging (default: null sink) + * @return Transaction result code + */ [[nodiscard]] TER checkInsufficientReserve( ReadView const& view, @@ -68,60 +114,97 @@ checkInsufficientReserve( SLE::const_ref accSle, STAmount const& accBalance, SLE::const_ref sponsorSle, - std::int32_t ownerCountDelta, - std::int32_t reserveCountDelta = 0, + std::int32_t ownerCountAdj, + std::int32_t accountCountAdj = 0, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); +/** Return number of the objects which reserve is covered by the account(sle) (so called "owner + * count"). Actual owner count can be adjusted by delta in ownerCountAdj. + * + * @param sle The account's ledger entry + * @param j Journal for logging + * @param ownerCountAdj Adjustment to the owner count (default: 0) + * @return The adjusted owner count + */ std::uint32_t -ownerCount( - ReadView const& view, - SLE::const_ref sle, - beast::Journal j, - std::int32_t ownerCountAdj = 0); +ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj = 0); -/** Adjust the owner count up or down. */ +/** Adjust the owner counters of the account up or down. If sponsor provided adjust its counters + * too. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param sponsorSle The sponsor's ledger entry (if applicable) + * @param accountCountAdj Adjustment amount for the account count + * @param j Journal for logging (default: null sink) + */ void adjustOwnerCount( ApplyView& view, SLE::ref accountSle, SLE::ref sponsorSle, - std::int32_t amount, + std::int32_t accountCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); +/** Convenience overload that accepts AccountID instead of SLE references. + * + * @param view The apply view for making changes + * @param account The account ID + * @param sponsor The optional sponsor account ID + * @param accountCountAdj Adjustment amount for the account count + * @param j Journal for logging (default: null sink) + */ inline void adjustOwnerCount( ApplyView& view, AccountID const& account, std::optional const& sponsor, - std::int32_t amount, + std::int32_t accountCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { adjustOwnerCount( view, view.peek(keylet::account(account)), sponsor ? view.peek(keylet::account(*sponsor)) : SLE::pointer(), - amount, + accountCountAdj, j); } +/** Adjust the owner counters of the account up or down. If object has sponsor adjust its counters + * too. Used primarily just before deleting the object. + * + * @param view The apply view for making changes + * @param accountSle The account's ledger entry + * @param objectSle The object's ledger entry + * @param accountCountAdj Adjustment amount for the account count + * @param j Journal for logging (default: null sink) + */ void adjustOwnerCountObj( ApplyView& view, SLE::ref accountSle, SLE::ref objectSle, - std::int32_t amount, + std::int32_t accountCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); +/** Convenience overload that accepts AccountID instead of account SLE reference. + * + * @param view The apply view for making changes + * @param account The account ID + * @param objectSle The object's ledger entry + * @param accountCountAdj Adjustment amount for the account count + * @param j Journal for logging (default: null sink) + */ inline void adjustOwnerCountObj( ApplyView& view, AccountID const& account, SLE::ref objectSle, - std::int32_t amount, + std::int32_t accountCountAdj, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { SLE::ref accountSle = view.peek(keylet::account(account)); - adjustOwnerCountObj(view, accountSle, objectSle, amount, j); + adjustOwnerCountObj(view, accountSle, objectSle, accountCountAdj, j); } /** Returns IOU issuer transfer fee as Rate. Rate specifies @@ -142,25 +225,25 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); /** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if set. - The list is constructed during initialization and is const after that. - Pseudo-account designator fields MUST be maintained by including the - SField::sMD_PseudoAccount flag in the SField definition. + The list is constructed during initialization and is const after that. + Pseudo-account designator fields MUST be maintained by including the + SField::sMD_PseudoAccount flag in the SField definition. */ [[nodiscard]] std::vector const& getPseudoAccountFields(); -/** Returns true if and only if sleAcct is a pseudo-account or specific - pseudo-accounts in pseudoFieldFilter. - - Returns false if sleAcct is: - - NOT a pseudo-account OR - - NOT a ltACCOUNT_ROOT OR - - null pointer -*/ +/** Convenience overload that reads the account from the view. */ [[nodiscard]] bool isPseudoAccount(SLE::const_ref sleAcct, std::set const& pseudoFieldFilter = {}); -/** Convenience overload that reads the account from the view. */ +/** Convenience overload that reads the account from the view. + * + * @param view The ledger view to read from + * @param accountId The account ID to check + * @param pseudoFieldFilter Optional set of specific pseudo-account fields to filter (default: + * empty) + * @return true if the account is a pseudo-account (or matches the filter), false otherwise + */ [[nodiscard]] inline bool isPseudoAccount( ReadView const& view, @@ -183,8 +266,8 @@ createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const /** Checks the destination and tag. - - Checks that the SLE is not null. - - If the SLE requires a destination tag, checks that there is a tag. +- Checks that the SLE is not null. +- If the SLE requires a destination tag, checks that there is a tag. */ [[nodiscard]] TER checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index 549644764f..0cfbbde538 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -36,13 +36,13 @@ checkFields(STTx const& tx, beast::Journal j); TER valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j); -// Check if subject has any credential maching the given domain. If you call it +// Check if subject has any credential matching the given domain. If you call it // in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in // doApply. This will ensure that expired credentials are deleted. TER validDomain(ReadView const& view, uint256 domainID, AccountID const& subject); -// This function is only called when we about to return tecNO_PERMISSION +// This function is only called when we are about to return tecNO_PERMISSION // because all the checks for the DepositPreauth authorization failed. TER authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst); @@ -58,7 +58,7 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j); } // namespace credentials -// Check expired credentials and for credentials maching DomainID of the ledger +// Check expired credentials and for credentials matching DomainID of the ledger // object TER verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j); diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h index 9cdad7173d..a517eefdaa 100644 --- a/include/xrpl/ledger/helpers/DelegateHelpers.h +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h @@ -23,13 +23,9 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx); * @param delegate The delegate account. * @param type Used to determine which granted granular permissions to load, * based on the transaction type. - * @param granularPermissions Granted granular permissions tied to the - * transaction type. + * @return the granted granular permissions tied to the transaction type. */ -void -loadGranularPermission( - SLE::const_ref delegate, - TxType const& type, - std::unordered_set& granularPermissions); +std::unordered_set +getGranularPermission(SLE::const_ref delegate, TxType const& type); } // namespace xrpl diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 86036fd023..362cfe5a8c 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -39,7 +39,7 @@ findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftoken /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft); +insertToken(ApplyView& view, AccountID owner, STObject&& nft); /** Remove the token from the owner's token directory. */ TER @@ -107,7 +107,6 @@ tokenOfferCreatePreclaim( TER tokenOfferCreateApply( ApplyView& view, - STTx const& tx, AccountID const& acctID, STAmount const& amount, std::optional const& dest, diff --git a/include/xrpl/ledger/helpers/OracleHelpers.h b/include/xrpl/ledger/helpers/OracleHelpers.h new file mode 100644 index 0000000000..635e47c564 --- /dev/null +++ b/include/xrpl/ledger/helpers/OracleHelpers.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace xrpl { + +constexpr uint32_t kMinOracleReserveCount = 1; +constexpr uint32_t kMaxOracleReserveCount = 2; +constexpr std::size_t kOracleReserveCountThreshold = 5; + +inline uint32_t +calculateOracleReserve(std::size_t priceDataSeriesCount) +{ + return priceDataSeriesCount > kOracleReserveCountThreshold ? kMaxOracleReserveCount + : kMinOracleReserveCount; +} + +} // namespace xrpl diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 810907b0af..3c08ee9f32 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -5,9 +5,45 @@ #include #include +#include +#include +#include + namespace xrpl { +/** Close a payment channel and return its remaining funds to the channel owner. + * + * @param slep The SLE for the PayChannel object to close. + * @param view The apply view in which ledger state modifications are made. + * @param key The ledger key identifying the PayChannel entry. + * @param j Journal used for fatal-level diagnostic messages. + * @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal + * fails; tefINTERNAL if the source account SLE cannot be found. + */ TER closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal j); +/** Add two uint32_t values with saturation at UINT32_MAX. + * + * @param rules The current ledger rules used to check amendment status. + * @param lhs Left-hand operand. + * @param rhs Right-hand operand. + * @return @p lhs + @p rhs, saturated at UINT32_MAX when the amendment + * is active. + */ +uint32_t +saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs); + +/** Determine whether a payment channel time field represents an expired time. + * + * @param view The apply view providing the parent close time and rules. + * @param timeField The optional expiry timestamp (seconds since the XRP + * Ledger epoch). If empty, the function returns false. + * @return @c true if @p timeField is set and the indicated time is + * in the past relative to the view's parent close time; + * @c false otherwise. + */ +bool +isChannelExpired(ApplyView const& view, std::optional timeField); + } // namespace xrpl diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index 525f109484..93614e5e17 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -13,17 +14,15 @@ namespace xrpl { inline bool -isReserveSponsored(STTx const& tx) +isFeeSponsored(STTx const& tx) { - return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u; + return (tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u; } inline bool -isSponsorReserveCoSigning(STTx const& tx) +isReserveSponsored(STTx const& tx) { - if (!tx.isFieldPresent(sfSponsorSignature)) - return false; - return isReserveSponsored(tx); + return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u; } inline std::optional @@ -125,13 +124,99 @@ removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor) sle->makeFieldAbsent(field); } -// namespace sponsor -// { -// // Accessing the ledger to check if provided sponsor is valid. -// [[nodiscard]] TER -// valid(ReadView const& view, STTx const& tx, beast::Journal j) -// { -// } -// } +template +inline std::optional +getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account) +{ + switch (sle->getType()) + { + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + return sle->getAccountID(sfAccount); + case ltMPTOKEN_ISSUANCE: + return sle->getAccountID(sfIssuer); + case ltSIGNER_LIST: { + auto const signerList = view.read(keylet::signers(account)); + if (!signerList) + return std::nullopt; + if (signerList->key() == sle->key()) + return account; + return std::nullopt; + } + case ltCREDENTIAL: { + if (sle->isFlag(lsfAccepted)) + return sle->getAccountID(sfSubject); + return sle->getAccountID(sfIssuer); + } + case ltRIPPLE_STATE: { + if (sle->isFlag(lsfHighReserve)) + { + auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == account) + return highAccount; + } + if (sle->isFlag(lsfLowReserve)) + { + auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == account) + return lowAccount; + } + return std::nullopt; + } + default: + UNREACHABLE("Object is not supported by sponsorship."); + return std::nullopt; + }; +} + +template +inline std::uint32_t +getLedgerEntryOwnerCount(T const& sle) +{ + switch (sle->getType()) + { + case ltORACLE: { + return calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); + } + // Vaults require 2 owner counts (the vault and a pseudo-account) + case ltVAULT: + return 2; + default: + return 1; + } +}; + +template +inline SF_ACCOUNT const& +getLedgerEntrySponsorField(T const& sle, AccountID const& owner) +{ + switch (sle->getType()) + { + case ltRIPPLE_STATE: { + if (sle->isFlag(lsfHighReserve)) + { + auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); + if (highAccount == owner) + return sfHighSponsor; + } + if (sle->isFlag(lsfLowReserve)) + { + auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); + if (lowAccount == owner) + return sfLowSponsor; + } + // LCOV_EXCL_START + UNREACHABLE("Should not happen. Owner should be checked before calling this function."); + return sfSponsor; + // LCOV_EXCL_STOP + } + default: + return sfSponsor; + } +}; } // namespace xrpl diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index 345049b377..10b7571641 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -102,25 +102,32 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled) json::Value const maxVersion( betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion); - if (jv.isObject()) + if (!jv.isObject() || !jv.isMember(jss::api_version)) + return RPC::kApiVersionIfUnspecified; + + try { - if (jv.isMember(jss::api_version)) + auto const& rawVersion = jv[jss::api_version]; + switch (rawVersion.type()) { - auto const specifiedVersion = jv[jss::api_version]; - if (!specifiedVersion.isInt() && !specifiedVersion.isUInt()) - { - return RPC::kApiInvalidVersion; + case json::ValueType::Int: + if (rawVersion.asInt() < 0) + return RPC::kApiInvalidVersion; + [[fallthrough]]; + case json::ValueType::UInt: { + auto const apiVersion = rawVersion.asUInt(); + if (apiVersion < kMinVersion || apiVersion > maxVersion) + return RPC::kApiInvalidVersion; + return apiVersion; } - auto const specifiedVersionInt = specifiedVersion.asInt(); - if (specifiedVersionInt < kMinVersion || specifiedVersionInt > maxVersion) - { + default: return RPC::kApiInvalidVersion; - } - return specifiedVersionInt; } } - - return RPC::kApiVersionIfUnspecified; + catch (...) + { + return RPC::kApiInvalidVersion; + } } } // namespace RPC diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 0da050b9e8..3fec2b7918 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -153,7 +153,7 @@ signers(AccountID const& account) noexcept; /** A Sponsorship */ Keylet -sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept; +sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept; /** A Check */ /** @{ */ diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 412dd78159..d65efd9389 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -180,12 +180,12 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfMPTCanClawback, 0x00000040)) \ \ LEDGER_OBJECT(MPTokenIssuanceMutable, \ - LSF_FLAG(lsmfMPTCanMutateCanLock, 0x00000002) \ - LSF_FLAG(lsmfMPTCanMutateRequireAuth, 0x00000004) \ - LSF_FLAG(lsmfMPTCanMutateCanEscrow, 0x00000008) \ - LSF_FLAG(lsmfMPTCanMutateCanTrade, 0x00000010) \ - LSF_FLAG(lsmfMPTCanMutateCanTransfer, 0x00000020) \ - LSF_FLAG(lsmfMPTCanMutateCanClawback, 0x00000040) \ + LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \ + LSF_FLAG(lsmfMPTCanEnableRequireAuth, 0x00000004) \ + LSF_FLAG(lsmfMPTCanEnableCanEscrow, 0x00000008) \ + LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \ + LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \ + LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \ LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \ LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \ \ diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 5d56fa4461..eb161ef7ad 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -7,8 +7,13 @@ #include #include #include +#include +#include namespace xrpl { + +class STTx; + /** * We have both transaction type permissions and granular type permissions. * Since we will reuse the TransactionFormats to parse the Transaction @@ -19,15 +24,15 @@ namespace xrpl { // Macro-generated, complex // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum GranularPermissionType : std::uint32_t { -#pragma push_macro("PERMISSION") -#undef PERMISSION +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION -#define PERMISSION(type, txType, value) type = (value), +#define GRANULAR_PERMISSION(name, txType, value, ...) name = (value), #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; // Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor @@ -40,15 +45,30 @@ class Permission private: Permission(); - std::unordered_map txFeatureMap_; + struct GranularPermissionEntry + { + std::string name; + TxType txType; + std::uint32_t permittedFlags; + SOTemplate permittedFields; - std::unordered_map delegableTx_; + GranularPermissionEntry( + std::string name, + TxType txType, + std::uint32_t permittedFlags, + std::vector fields); + }; - std::unordered_map granularPermissionMap_; + struct TxDelegationEntry + { + uint256 amendment; + Delegation delegable{NotDelegable}; + }; - std::unordered_map granularNameMap_; - - std::unordered_map granularTxTypeMap_; + std::unordered_set granularTxTypes_; + std::unordered_map txDelegationMap_; + std::unordered_map granularPermissionsByName_; + std::unordered_map granularPermissions_; public: static Permission const& @@ -59,30 +79,52 @@ public: operator=(Permission const&) = delete; [[nodiscard]] std::optional - getPermissionName(std::uint32_t const value) const; + getPermissionName(std::uint32_t value) const; [[nodiscard]] std::optional getGranularValue(std::string const& name) const; [[nodiscard]] std::optional - getGranularName(GranularPermissionType const& value) const; + getGranularName(GranularPermissionType value) const; [[nodiscard]] std::optional - getGranularTxType(GranularPermissionType const& gpType) const; + getGranularTxType(GranularPermissionType gpType) const; + // Returns a reference to avoid copying uint256 - 32 bytes. std::optional + // cannot hold references directly, so std::reference_wrapper is used. [[nodiscard]] std::optional> getTxFeature(TxType txType) const; [[nodiscard]] bool - isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const; + isDelegable(std::uint32_t permissionValue, Rules const& rules) const; + + [[nodiscard]] bool + hasGranularPermissions(TxType txType) const; // for tx level permission, permission value is equal to tx type plus one - static uint32_t - txToPermissionType(TxType const& type); + [[nodiscard]] static uint32_t + txToPermissionType(TxType type); // tx type value is permission value minus one - static TxType - permissionToTxType(uint32_t const& value); + [[nodiscard]] static TxType + permissionToTxType(std::uint32_t value); + + /** + * @brief Verifies a delegated transaction against its granular permission template. + * + * @note WARNING: Do not move this check before standard transaction-level + * format checks, which is in preclaim. This function assumes the transaction's + * base structural integrity (fees, sequence, signatures) has already been + * validated. + * + * @param tx The transaction to verify. + * @param heldPermissions The granular permissions that the sender hold. + * @return true if the transaction fields and flags comply with the granular template. + */ + [[nodiscard]] bool + checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const; }; } // namespace xrpl diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 659fede31d..3aa448d2e7 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -84,7 +84,7 @@ public: getSeqValue() const; AccountID - getFeePayer() const; + getInitiator() const; boost::container::flat_set getMentionedAccounts() const; diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f300c253ca..212063b9db 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -353,44 +353,35 @@ getAllTxFlags() inline constexpr FlagValue tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment); inline constexpr FlagValue tfTrustSetPermissionMask = ~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze); -inline constexpr FlagValue tfSponsorshipSetPermissionMask = - ~(tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve | - tfSponsorshipClearRequireSignForFee | tfSponsorshipClearRequireSignForReserve); // MPTokenIssuanceCreate MutableFlags: // Indicating specific fields or flags may be changed after issuance. -inline constexpr FlagValue tmfMPTCanMutateCanLock = lsmfMPTCanMutateCanLock; -inline constexpr FlagValue tmfMPTCanMutateRequireAuth = lsmfMPTCanMutateRequireAuth; -inline constexpr FlagValue tmfMPTCanMutateCanEscrow = lsmfMPTCanMutateCanEscrow; -inline constexpr FlagValue tmfMPTCanMutateCanTrade = lsmfMPTCanMutateCanTrade; -inline constexpr FlagValue tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTransfer; -inline constexpr FlagValue tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback; +inline constexpr FlagValue tmfMPTCanEnableCanLock = lsmfMPTCanEnableCanLock; +inline constexpr FlagValue tmfMPTCanEnableRequireAuth = lsmfMPTCanEnableRequireAuth; +inline constexpr FlagValue tmfMPTCanEnableCanEscrow = lsmfMPTCanEnableCanEscrow; +inline constexpr FlagValue tmfMPTCanEnableCanTrade = lsmfMPTCanEnableCanTrade; +inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTransfer; +inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback; inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata; inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee; inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask = - ~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow | - tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | + ~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow | + tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee); // MPTokenIssuanceSet MutableFlags: -// Set or Clear flags. +// Enable mutable capability flags. These flags are one-way: once enabled, +// the corresponding capability cannot be disabled by MPTokenIssuanceSet. inline constexpr FlagValue tmfMPTSetCanLock = 0x00000001; -inline constexpr FlagValue tmfMPTClearCanLock = 0x00000002; -inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000004; -inline constexpr FlagValue tmfMPTClearRequireAuth = 0x00000008; -inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000010; -inline constexpr FlagValue tmfMPTClearCanEscrow = 0x00000020; -inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000040; -inline constexpr FlagValue tmfMPTClearCanTrade = 0x00000080; -inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000100; -inline constexpr FlagValue tmfMPTClearCanTransfer = 0x00000200; -inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000400; -inline constexpr FlagValue tmfMPTClearCanClawback = 0x00000800; -inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~( - tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | - tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade | - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | tmfMPTClearCanClawback); +inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000002; +inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004; +inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008; +inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010; +inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020; +inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = + ~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade | + tmfMPTSetCanTransfer | tmfMPTSetCanClawback); // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a // TrustLine to be added to the issuer of that token without explicit permission from that issuer. @@ -463,34 +454,11 @@ getAsfFlagMap() #pragma pop_macro("ACCOUNTSET_FLAG_TO_MAP") #pragma pop_macro("ACCOUNTSET_FLAGS") -#pragma push_macro("SPONSOR_FLAGS") -#pragma push_macro("SPONSOR_FLAG_TO_VALUE") -#pragma push_macro("SPONSOR_FLAG_TO_MAP") +// Sponsor flags (spf) -// Sponsor Flag values -#define SPONSOR_FLAGS(SPF_FLAG) \ - SPF_FLAG(spfSponsorFee, 1) \ - SPF_FLAG(spfSponsorReserve, 2) - -#define SPONSOR_FLAG_TO_VALUE(name, value) inline constexpr FlagValue name = value; -#define SPONSOR_FLAG_TO_MAP(name, value) {#name, value}, - -SPONSOR_FLAGS(SPONSOR_FLAG_TO_VALUE) - -inline std::map const& -getspfFlagMap() -{ - static std::map const flags = {SPONSOR_FLAGS(SPONSOR_FLAG_TO_MAP)}; - return flags; -} - -#undef SPONSOR_FLAG_TO_VALUE -#undef SPONSOR_FLAG_TO_MAP -#undef SPONSOR_FLAGS - -#pragma pop_macro("SPONSOR_FLAG_TO_VALUE") -#pragma pop_macro("SPONSOR_FLAG_TO_MAP") -#pragma pop_macro("SPONSOR_FLAGS") +inline constexpr FlagValue spfSponsorFee = 1; +inline constexpr FlagValue spfSponsorReserve = 2; +inline constexpr FlagValue spfSponsorFlagMask = ~(spfSponsorFee | spfSponsorReserve); } // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c90e4732dc..a8a954ebec 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -22,10 +22,10 @@ XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultN XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) XRPL_FIX (BatchInnerSigs, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(PermissionDelegationV1_1, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (IncludeKeyletFields, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(DynamicMPT, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(DynamicMPT, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (TokenEscrowV1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (PriceOracleOrder, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 7c534067a0..7daac16f04 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -127,32 +127,32 @@ LEDGER_ENTRY(ltTICKET, 0x0054, Ticket, ticket, ({ \sa keylet::account */ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ - {sfAccount, SoeRequired}, - {sfSequence, SoeRequired}, - {sfBalance, SoeRequired}, - {sfOwnerCount, SoeRequired}, - {sfPreviousTxnID, SoeRequired}, - {sfPreviousTxnLgrSeq, SoeRequired}, - {sfAccountTxnID, SoeOptional}, - {sfRegularKey, SoeOptional}, - {sfEmailHash, SoeOptional}, - {sfWalletLocator, SoeOptional}, - {sfWalletSize, SoeOptional}, - {sfMessageKey, SoeOptional}, - {sfTransferRate, SoeOptional}, - {sfDomain, SoeOptional}, - {sfTickSize, SoeOptional}, - {sfTicketCount, SoeOptional}, - {sfNFTokenMinter, SoeOptional}, - {sfMintedNFTokens, SoeDefault}, - {sfBurnedNFTokens, SoeDefault}, - {sfFirstNFTokenSequence, SoeOptional}, - {sfAMMID, SoeOptional}, // pseudo-account designator - {sfVaultID, SoeOptional}, // pseudo-account designator - {sfLoanBrokerID, SoeOptional}, // pseudo-account designator - {sfSponsoredOwnerCount, SoeDefault}, - {sfSponsoringOwnerCount, SoeDefault}, - {sfSponsoringAccountCount,SoeDefault}, + {sfAccount, SoeRequired}, + {sfSequence, SoeRequired}, + {sfBalance, SoeRequired}, + {sfOwnerCount, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfAccountTxnID, SoeOptional}, + {sfRegularKey, SoeOptional}, + {sfEmailHash, SoeOptional}, + {sfWalletLocator, SoeOptional}, + {sfWalletSize, SoeOptional}, + {sfMessageKey, SoeOptional}, + {sfTransferRate, SoeOptional}, + {sfDomain, SoeOptional}, + {sfTickSize, SoeOptional}, + {sfTicketCount, SoeOptional}, + {sfNFTokenMinter, SoeOptional}, + {sfMintedNFTokens, SoeDefault}, + {sfBurnedNFTokens, SoeDefault}, + {sfFirstNFTokenSequence, SoeOptional}, + {sfSponsoredOwnerCount, SoeDefault}, + {sfSponsoringOwnerCount, SoeDefault}, + {sfSponsoringAccountCount, SoeDefault}, + {sfAMMID, SoeOptional}, // pseudo-account designator + {sfVaultID, SoeOptional}, // pseudo-account designator + {sfLoanBrokerID, SoeOptional}, // pseudo-account designator })) /** A ledger object which contains a list of object identifiers. @@ -613,7 +613,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ })) /** A ledger object representing a sponsorship. - \sa keylet::sponsor + \sa keylet::sponsorship */ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({ {sfPreviousTxnID, SoeRequired}, @@ -622,7 +622,7 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({ {sfSponsee, SoeRequired}, {sfFeeAmount, SoeOptional}, {sfMaxFee, SoeOptional}, - {sfReserveCount, SoeDefault}, + {sfRemainingOwnerCount, SoeDefault}, {sfOwnerNode, SoeRequired}, {sfSponseeNode, SoeRequired}, })) diff --git a/include/xrpl/protocol/detail/permissions.macro b/include/xrpl/protocol/detail/permissions.macro index 6f31ced50b..d64e09024b 100644 --- a/include/xrpl/protocol/detail/permissions.macro +++ b/include/xrpl/protocol/detail/permissions.macro @@ -1,55 +1,87 @@ -#if !defined(PERMISSION) -#error "undefined macro: PERMISSION" +#if !defined(GRANULAR_PERMISSION) +#error "undefined macro: GRANULAR_PERMISSION" #endif /** - * PERMISSION(name, type, txType, value) + * GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields) * - * This macro defines a permission: - * name: the name of the permission. - * type: the GranularPermissionType enum. - * txType: the corresponding TxType for this permission. - * value: the uint32 numeric value for the enum type. + * Defines a granular permission: + * name: the granular permission name. + * txType: the corresponding TxType for this permission. + * value: the uint32 numeric value for the enum type. + * allowedFlags: transaction flags permitted under this permission. + * allowedFields: transaction fields permitted under this permission. */ -/** This permission grants the delegated account the ability to authorize a trustline. */ -PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537) +/** Grants the ability to authorize a trustline. */ +GRANULAR_PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537, tfUniversal | tfSetfAuth, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to freeze a trustline. */ -PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538) +/** Grants the ability to freeze a trustline. */ +GRANULAR_PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538, tfUniversal | tfSetFreeze, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to unfreeze a trustline. */ -PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539) +/** Grants the ability to unfreeze a trustline. */ +GRANULAR_PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539, tfUniversal | tfClearFreeze, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to set Domain. */ -PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540) +/** Grants the ability to set Domain. */ +GRANULAR_PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540, tfUniversal, + ({{sfDomain, SoeOptional}})) -/** This permission grants the delegated account the ability to set EmailHashSet. */ -PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541) +/** Grants the ability to set EmailHash. */ +GRANULAR_PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541, tfUniversal, + ({{sfEmailHash, SoeOptional}})) -/** This permission grants the delegated account the ability to set MessageKey. */ -PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542) +/** Grants the ability to set MessageKey. */ +GRANULAR_PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542, tfUniversal, + ({{sfMessageKey, SoeOptional}})) -/** This permission grants the delegated account the ability to set TransferRate. */ -PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543) +/** Grants the ability to set TransferRate. */ +GRANULAR_PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543, tfUniversal, + ({{sfTransferRate, SoeOptional}})) -/** This permission grants the delegated account the ability to set TickSize. */ -PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544) +/** Grants the ability to set TickSize. */ +GRANULAR_PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544, tfUniversal, + ({{sfTickSize, SoeOptional}})) -/** This permission grants the delegated account the ability to mint payment, which means sending a payment for a currency where the sending account is the issuer. */ -PERMISSION(PaymentMint, ttPAYMENT, 65545) +/** Grants the ability to mint payment (sending account is the issuer). Cross-currency payments are disallowed. */ +GRANULAR_PERMISSION(PaymentMint, ttPAYMENT, 65545, tfUniversal, + ({{sfDestination, SoeRequired}, + {sfAmount, SoeRequired}, + {sfSendMax, SoeOptional}, + {sfInvoiceID, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}})) -/** This permission grants the delegated account the ability to burn payment, which means sending a payment for a currency where the destination account is the issuer */ -PERMISSION(PaymentBurn, ttPAYMENT, 65546) +/** Grants the ability to burn payment (destination account is the issuer). Cross-currency payments are disallowed. */ +GRANULAR_PERMISSION(PaymentBurn, ttPAYMENT, 65546, tfUniversal, + ({{sfDestination, SoeRequired}, + {sfAmount, SoeRequired}, + {sfSendMax, SoeOptional}, + {sfInvoiceID, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}})) -/** This permission grants the delegated account the ability to lock MPToken. */ -PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547) +/** Grants the ability to lock an MPToken. */ +GRANULAR_PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547, tfUniversal | tfMPTLock, + ({{sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}})) -/** This permission grants the delegated account the ability to unlock MPToken. */ -PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548) +/** Grants the ability to unlock an MPToken. */ +GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock, + ({{sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}})) /** This permission grants the delegated account the ability to set SponsorFee. */ -PERMISSION(SponsorFee, ttSPONSORSHIP_SET, 65549) +GRANULAR_PERMISSION(SponsorFee, ttSPONSORSHIP_SET, 65549, + tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee, + ({{sfSponsee, SoeOptional}, + {sfFeeAmount, SoeOptional}, + {sfMaxFee, SoeOptional}})) /** This permission grants the delegated account the ability to set SponsorReserve. */ -PERMISSION(SponsorReserve, ttSPONSORSHIP_SET, 65550) +GRANULAR_PERMISSION(SponsorReserve, ttSPONSORSHIP_SET, 65550, + tfUniversal | tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForReserve, + ({{sfSponsee, SoeOptional}, + {sfRemainingOwnerCount, SoeOptional}})) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 4870d448c7..b1f6efb560 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -116,7 +116,7 @@ TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bi TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 69) TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 70) TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 71) -TYPED_SFIELD(sfReserveCount, UINT32, 72) +TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 72) TYPED_SFIELD(sfSponsorFlags, UINT32, 73) // 64-bit integers (common) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 73e5173876..c901a8e551 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1084,7 +1084,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, TRANSACTION(ttSPONSORSHIP_TRANSFER, 85, SponsorshipTransfer, Delegation::Delegable, featureSponsor, - NoPriv, + MayModifyVault, ({ {sfObjectID, SoeOptional}, {sfSponsee, SoeOptional}, @@ -1103,7 +1103,7 @@ TRANSACTION(ttSPONSORSHIP_SET, 86, SponsorshipSet, {sfSponsee, SoeOptional}, {sfFeeAmount, SoeOptional}, {sfMaxFee, SoeOptional}, - {sfReserveCount, SoeOptional}, + {sfRemainingOwnerCount, SoeOptional}, })) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h index b442e77fe0..725f46437e 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h @@ -447,78 +447,6 @@ public: return this->sle_->isFieldPresent(sfFirstNFTokenSequence); } - /** - * @brief Get sfAMMID (SoeOptional) - * @return The field value, or std::nullopt if not present. - */ - [[nodiscard]] - protocol_autogen::Optional - getAMMID() const - { - if (hasAMMID()) - return this->sle_->at(sfAMMID); - return std::nullopt; - } - - /** - * @brief Check if sfAMMID is present. - * @return True if the field is present, false otherwise. - */ - [[nodiscard]] - bool - hasAMMID() const - { - return this->sle_->isFieldPresent(sfAMMID); - } - - /** - * @brief Get sfVaultID (SoeOptional) - * @return The field value, or std::nullopt if not present. - */ - [[nodiscard]] - protocol_autogen::Optional - getVaultID() const - { - if (hasVaultID()) - return this->sle_->at(sfVaultID); - return std::nullopt; - } - - /** - * @brief Check if sfVaultID is present. - * @return True if the field is present, false otherwise. - */ - [[nodiscard]] - bool - hasVaultID() const - { - return this->sle_->isFieldPresent(sfVaultID); - } - - /** - * @brief Get sfLoanBrokerID (SoeOptional) - * @return The field value, or std::nullopt if not present. - */ - [[nodiscard]] - protocol_autogen::Optional - getLoanBrokerID() const - { - if (hasLoanBrokerID()) - return this->sle_->at(sfLoanBrokerID); - return std::nullopt; - } - - /** - * @brief Check if sfLoanBrokerID is present. - * @return True if the field is present, false otherwise. - */ - [[nodiscard]] - bool - hasLoanBrokerID() const - { - return this->sle_->isFieldPresent(sfLoanBrokerID); - } - /** * @brief Get sfSponsoredOwnerCount (SoeDefault) * @return The field value, or std::nullopt if not present. @@ -590,6 +518,78 @@ public: { return this->sle_->isFieldPresent(sfSponsoringAccountCount); } + + /** + * @brief Get sfAMMID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAMMID() const + { + if (hasAMMID()) + return this->sle_->at(sfAMMID); + return std::nullopt; + } + + /** + * @brief Check if sfAMMID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAMMID() const + { + return this->sle_->isFieldPresent(sfAMMID); + } + + /** + * @brief Get sfVaultID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVaultID() const + { + if (hasVaultID()) + return this->sle_->at(sfVaultID); + return std::nullopt; + } + + /** + * @brief Check if sfVaultID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVaultID() const + { + return this->sle_->isFieldPresent(sfVaultID); + } + + /** + * @brief Get sfLoanBrokerID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLoanBrokerID() const + { + if (hasLoanBrokerID()) + return this->sle_->at(sfLoanBrokerID); + return std::nullopt; + } + + /** + * @brief Check if sfLoanBrokerID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLoanBrokerID() const + { + return this->sle_->isFieldPresent(sfLoanBrokerID); + } }; /** @@ -858,39 +858,6 @@ public: return *this; } - /** - * @brief Set sfAMMID (SoeOptional) - * @return Reference to this builder for method chaining. - */ - AccountRootBuilder& - setAMMID(std::decay_t const& value) - { - object_[sfAMMID] = value; - return *this; - } - - /** - * @brief Set sfVaultID (SoeOptional) - * @return Reference to this builder for method chaining. - */ - AccountRootBuilder& - setVaultID(std::decay_t const& value) - { - object_[sfVaultID] = value; - return *this; - } - - /** - * @brief Set sfLoanBrokerID (SoeOptional) - * @return Reference to this builder for method chaining. - */ - AccountRootBuilder& - setLoanBrokerID(std::decay_t const& value) - { - object_[sfLoanBrokerID] = value; - return *this; - } - /** * @brief Set sfSponsoredOwnerCount (SoeDefault) * @return Reference to this builder for method chaining. @@ -924,6 +891,39 @@ public: return *this; } + /** + * @brief Set sfAMMID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setAMMID(std::decay_t const& value) + { + object_[sfAMMID] = value; + return *this; + } + + /** + * @brief Set sfVaultID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setVaultID(std::decay_t const& value) + { + object_[sfVaultID] = value; + return *this; + } + + /** + * @brief Set sfLoanBrokerID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + AccountRootBuilder& + setLoanBrokerID(std::decay_t const& value) + { + object_[sfLoanBrokerID] = value; + return *this; + } + /** * @brief Build and return the completed AccountRoot wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h index 96b46a6aab..c309a38aef 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Sponsorship.h @@ -138,27 +138,27 @@ public: } /** - * @brief Get sfReserveCount (SoeDefault) + * @brief Get sfRemainingOwnerCount (SoeDefault) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getReserveCount() const + getRemainingOwnerCount() const { - if (hasReserveCount()) - return this->sle_->at(sfReserveCount); + if (hasRemainingOwnerCount()) + return this->sle_->at(sfRemainingOwnerCount); return std::nullopt; } /** - * @brief Check if sfReserveCount is present. + * @brief Check if sfRemainingOwnerCount is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasReserveCount() const + hasRemainingOwnerCount() const { - return this->sle_->isFieldPresent(sfReserveCount); + return this->sle_->isFieldPresent(sfRemainingOwnerCount); } /** @@ -297,13 +297,13 @@ public: } /** - * @brief Set sfReserveCount (SoeDefault) + * @brief Set sfRemainingOwnerCount (SoeDefault) * @return Reference to this builder for method chaining. */ SponsorshipBuilder& - setReserveCount(std::decay_t const& value) + setRemainingOwnerCount(std::decay_t const& value) { - object_[sfReserveCount] = value; + object_[sfRemainingOwnerCount] = value; return *this; } diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h index 0b64104e61..f0ef449de7 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -152,29 +152,29 @@ public: } /** - * @brief Get sfReserveCount (SoeOptional) + * @brief Get sfRemainingOwnerCount (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getReserveCount() const + getRemainingOwnerCount() const { - if (hasReserveCount()) + if (hasRemainingOwnerCount()) { - return this->tx_->at(sfReserveCount); + return this->tx_->at(sfRemainingOwnerCount); } return std::nullopt; } /** - * @brief Check if sfReserveCount is present. + * @brief Check if sfRemainingOwnerCount is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasReserveCount() const + hasRemainingOwnerCount() const { - return this->tx_->isFieldPresent(sfReserveCount); + return this->tx_->isFieldPresent(sfRemainingOwnerCount); } }; @@ -263,13 +263,13 @@ public: } /** - * @brief Set sfReserveCount (SoeOptional) + * @brief Set sfRemainingOwnerCount (SoeOptional) * @return Reference to this builder for method chaining. */ SponsorshipSetBuilder& - setReserveCount(std::decay_t const& value) + setRemainingOwnerCount(std::decay_t const& value) { - object_[sfReserveCount] = value; + object_[sfRemainingOwnerCount] = value; return *this; } diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h index 8ac43071ee..ff11a957a7 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h @@ -21,7 +21,7 @@ class SponsorshipTransferBuilder; * Type: ttSPONSORSHIP_TRANSFER (85) * Delegable: Delegation::Delegable * Amendment: featureSponsor - * Privileges: NoPriv + * Privileges: MayModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use SponsorshipTransferBuilder to construct new transactions. diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index e93676a938..f316885fd6 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,19 @@ public: }; /** Manages a client's subscription to data feeds. + * + * An InfoSub holds a non-owning reference to its `Source` (typically the + * process-wide `NetworkOPsImp`). The destructor reaches back into the + * `Source` to remove this subscriber from every server-side subscription + * map. + * + * @note Lifetime contract: every `InfoSub` instance MUST be destroyed + * before the backing `Source`. NetworkOPsImp shutdown drops all + * subscriber strong refs before its own teardown to satisfy this. + * @note Thread-safety: per-instance state is guarded by `lock_`. The + * destructor reads tracking sets without taking `lock_` because + * the strong-pointer ref-count is zero at destruction time, so + * no other thread can be calling the public mutators. */ class InfoSub : public CountedObject { @@ -117,8 +131,43 @@ public: virtual bool subBook(ref ispListener, Book const&) = 0; + + /** + * Remove a book subscription for a live subscriber. + * + * Clears the book from the subscriber's own tracking set + * (InfoSub::bookSubscriptions_) and then removes the server-side + * entry from subBook_. Call this from RPC unsubscribe handlers. + * + * @param ispListener The subscriber requesting removal. + * @param book The order book to unsubscribe from. + * @return true if the entry was present and removed, false if the + * subscriber was not subscribed to @p book. + * + * @note Thread-safety: acquires subLock_ internally. + * @note Do NOT call from ~InfoSub(). Use unsubBookInternal instead + * to avoid a redundant write-back to bookSubscriptions_ on a + * partially-destroyed object. + */ virtual bool - unsubBook(std::uint64_t uListener, Book const&) = 0; + unsubBook(ref ispListener, Book const&) = 0; + + /** + * Remove a book subscription during InfoSub teardown. + * + * Removes only the server-side entry from subBook_. Does NOT touch + * InfoSub::bookSubscriptions_ because the InfoSub is being destroyed. + * Called by ~InfoSub() for each book in bookSubscriptions_. + * + * @param uListener The sequence number of the subscriber being torn down. + * @param book The order book entry to remove. + * @return true if the entry was present and removed, false otherwise + * (e.g., already removed by a concurrent RPC unsubscribe). + * + * @note Thread-safety: acquires subLock_ internally. + */ + virtual bool + unsubBookInternal(std::uint64_t uListener, Book const&) = 0; virtual bool subTransactions(ref ispListener) = 0; @@ -158,6 +207,13 @@ public: addRpcSub(std::string const& strUrl, ref rspEntry) = 0; virtual bool tryRemoveRpcSub(std::string const& strUrl) = 0; + + /** Journal used by InfoSub for diagnostics that occur after the + * owning subsystem (e.g. application-level Logs) is the only + * surviving sink — primarily destructor-time cleanup failures. + */ + [[nodiscard]] virtual beast::Journal const& + journal() const = 0; }; public: @@ -184,6 +240,31 @@ public: void deleteSubAccountInfo(AccountID const& account, bool rt); + /** Record that this subscriber is following @p book. + * + * Called by NetworkOPsImp::subBook so that ~InfoSub() can issue a + * matching unsubBook for every book this subscriber is tracking, + * keeping per-subscriber state symmetric with the server-side map. + * + * @param book The order book this subscriber has just subscribed to. + * @note Idempotent: re-inserting an already-tracked book is a no-op. + * @note Thread-safe: takes InfoSub::lock_. + */ + void + insertBookSubscription(Book const& book); + + /** Stop tracking @p book for this subscriber. + * + * Called by the unsubscribe RPC handler so that the book is not + * re-unsubscribed by ~InfoSub(). Pairs with insertBookSubscription. + * + * @param book The order book to forget. + * @note No-op if @p book was not previously inserted. + * @note Thread-safe: takes InfoSub::lock_. + */ + void + deleteBookSubscription(Book const& book); + // return false if already subscribed to this account bool insertSubAccountHistory(AccountID const& account); @@ -217,6 +298,7 @@ private: std::shared_ptr request_; std::uint64_t seq_; hash_set accountHistorySubscriptions_; + hash_set bookSubscriptions_; unsigned int apiVersion_ = 0; static int diff --git a/include/xrpl/server/NetworkOPs.h b/include/xrpl/server/NetworkOPs.h index e2aa17566e..785d808935 100644 --- a/include/xrpl/server/NetworkOPs.h +++ b/include/xrpl/server/NetworkOPs.h @@ -249,6 +249,19 @@ public: virtual void stateAccounting(json::Value& obj) = 0; + + /** Total number of (book, subscriber) entries currently tracked. + * + * Counts every weak_ptr stored across every book in subBook_, NOT the + * number of distinct subscribers and NOT the number of distinct + * books: a single subscriber following N books contributes N entries. + * + * @note Diagnostic accessor; intended for tests and operator visibility + * into per-book subscription state. The returned value is a + * snapshot under the subscription lock. + */ + virtual std::size_t + getBookSubscribersCount() = 0; }; } // namespace xrpl diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index d419e56128..cf5276300a 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -8,6 +8,7 @@ #include #include +#include #include namespace xrpl { @@ -237,8 +238,63 @@ public: return tesSUCCESS; } + /** + * This function can be overridden to introduce additional semantic constraints beyond the + * granular template validation for granular permissions. It is called by the base + * invokeCheckPermission method only after the transaction has successfully passed + * checkGranularSandbox. + */ static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) + { + return tesSUCCESS; + } + + /** + * Checks whether the transaction is authorized to be executed by the delegated account. + * This function enforces the strict permission check hierarchy. It is explicitly + * designed NOT to be overridden. Derived transactors must instead implement + * checkGranularSemantics to add custom validation logic for granular permissions. + * + * The evaluation proceeds as follows: + * - If transaction-level permission is granted, the function immediately returns tesSUCCESS. + * - If transaction-level permission is not granted, the function checks whether the transaction + * matches the granular permission template defined in permissions.macro. If it does, it then + * calls checkGranularSemantics to perform any additional, fine-grained validation. + * + */ + template + static NotTEC + invokeCheckPermission(ReadView const& view, STTx const& tx) + { + // heldGranularPermissions is passed by reference into checkPermission. + // It is populated with the sender’s granular permissions only when the sender + // lacks tx-level permission but has granular permissions that satisfy the + // granular permission template. + // + // - result is terNO_DELEGATE_PERMISSION: return immediately. + // - result is tesSUCCESS and heldGranularPermissions is empty: tx-level permission was + // granted, so we returned success before populating it. + // - result is tesSUCCESS and heldGranularPermissions is not empty: tx-level permission was + // not granted, but the held granular permissions passed checkGranularSandbox, so we proceed + // to checkGranularSemantics. + // + // WARNING: Do not simplify checkPermission to return only + // heldGranularPermissions or the ter code. Both the result and the + // populated set are required to enforce the strict permission hierarchy + // described above. + std::unordered_set heldGranularPermissions; + if (NotTEC const result = checkPermission(view, tx, heldGranularPermissions); + !isTesSuccess(result) || heldGranularPermissions.empty()) + { + return result; + } + + return T::checkGranularSemantics(view, tx, heldGranularPermissions); + } static NotTEC checkSponsor(ReadView const& view, STTx const& tx); @@ -371,6 +427,12 @@ protected: unit::ValueUnit min = unit::ValueUnit{}); private: + static NotTEC + checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions); + std::pair reset(XRPAmount fee); @@ -379,8 +441,13 @@ private: TER consumeSeqProxy(SLE::pointer const& sleAccount); + TER payFee(); + + std::tuple + processPersistentChanges(TER result, XRPAmount fee); + static NotTEC checkSingleSign( ReadView const& view, @@ -388,6 +455,7 @@ private: AccountID const& idAccount, SLE::const_pointer sleAccount, beast::Journal const j); + static NotTEC checkMultiSign( ReadView const& view, diff --git a/include/xrpl/tx/invariants/AMMInvariant.h b/include/xrpl/tx/invariants/AMMInvariant.h index ee2fb66a1c..4b56370774 100644 --- a/include/xrpl/tx/invariants/AMMInvariant.h +++ b/include/xrpl/tx/invariants/AMMInvariant.h @@ -15,7 +15,9 @@ class ValidAMM std::optional ammAccount_; std::optional lptAMMBalanceAfter_; std::optional lptAMMBalanceBefore_; + std::optional lptAMMBalanceBeforeDeletion_; bool ammPoolChanged_{false}; + bool ammDeleted_{false}; public: enum class ZeroAllowed : bool { No = false, Yes = true }; @@ -35,12 +37,17 @@ private: [[nodiscard]] bool finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; [[nodiscard]] bool - finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + finalizeDelete(bool enforce, bool enforceAMMDelete, TER res, beast::Journal const&) const; [[nodiscard]] bool finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; // Includes clawback [[nodiscard]] bool - finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const; + finalizeWithdraw( + STTx const&, + ReadView const&, + bool enforce, + bool enforceAMMDelete, + beast::Journal const&) const; [[nodiscard]] bool finalizeDEX(bool enforce, beast::Journal const&) const; [[nodiscard]] bool diff --git a/include/xrpl/tx/invariants/SponsorshipInvariant.h b/include/xrpl/tx/invariants/SponsorshipInvariant.h index 8bae9d8d47..79dedfcd1d 100644 --- a/include/xrpl/tx/invariants/SponsorshipInvariant.h +++ b/include/xrpl/tx/invariants/SponsorshipInvariant.h @@ -22,7 +22,7 @@ class SponsorshipOwnerCountsMatch std::int64_t deltaSponsoredOwnerCount_ = 0; std::int64_t deltaSponsoringOwnerCount_ = 0; std::int64_t deltaSponsoredObjectOwnerCount_ = 0; - std::uint64_t invalidOwnerCountLessThanSponsoredOwnerCount_ = 0; + std::uint64_t ownerCountBelowSponsored_ = 0; public: void diff --git a/include/xrpl/tx/transactors/account/AccountSet.h b/include/xrpl/tx/transactors/account/AccountSet.h index a40a9ec963..91b38e7968 100644 --- a/include/xrpl/tx/transactors/account/AccountSet.h +++ b/include/xrpl/tx/transactors/account/AccountSet.h @@ -23,9 +23,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 1973968adb..6e88320eae 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -100,7 +100,6 @@ public: static std::tuple> equalWithdrawTokens( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const account, AccountID const& ammAccount, @@ -135,7 +134,6 @@ public: static std::tuple> withdraw( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, AccountID const& account, @@ -179,7 +177,6 @@ private: std::pair withdraw( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -205,7 +202,6 @@ private: std::pair equalWithdrawTokens( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -231,7 +227,6 @@ private: std::pair equalWithdrawLimit( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -254,7 +249,6 @@ private: std::pair singleWithdraw( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -276,7 +270,6 @@ private: std::pair singleWithdrawTokens( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -299,7 +292,6 @@ private: std::pair singleWithdrawEPrice( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h b/include/xrpl/tx/transactors/oracle/OracleSet.h index 51ed7be3dc..831c11b8c4 100644 --- a/include/xrpl/tx/transactors/oracle/OracleSet.h +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h @@ -22,12 +22,6 @@ public: { } - static uint32_t - calculateOracleReserve(std::size_t count) - { - return count > 5 ? 2 : 1; - } - static NotTEC preflight(PreflightContext const& ctx); diff --git a/include/xrpl/tx/transactors/payment/Payment.h b/include/xrpl/tx/transactors/payment/Payment.h index 14897b4efe..dd792aa1c2 100644 --- a/include/xrpl/tx/transactors/payment/Payment.h +++ b/include/xrpl/tx/transactors/payment/Payment.h @@ -32,7 +32,10 @@ public: preflight(PreflightContext const& ctx); static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions); static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h index d7e78da2ca..53aa74d3dd 100644 --- a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -19,9 +19,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h index 6a6d1fc445..428c573e2f 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h @@ -22,9 +22,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/token/TrustSet.h b/include/xrpl/tx/transactors/token/TrustSet.h index dcf454bea1..d719f06326 100644 --- a/include/xrpl/tx/transactors/token/TrustSet.h +++ b/include/xrpl/tx/transactors/token/TrustSet.h @@ -21,7 +21,10 @@ public: preflight(PreflightContext const& ctx); static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions); static TER preclaim(PreclaimContext const& ctx); diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index e6df48e18c..6d8980f897 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -71,7 +71,7 @@ if [ ! -e "${target}" ]; then fi EOF -COPY nix/docker/check-tools.sh /tmp/check-tools.sh +COPY bin/check-tools.sh /tmp/check-tools.sh RUN /tmp/check-tools.sh # Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones. @@ -93,7 +93,7 @@ RUN if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then \ SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] # Sanity-check that the built binaries run correctly in the vanilla base image, with the necessary sanitizer runtime libraries installed. -COPY nix/docker/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh +COPY bin/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh COPY nix/docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh COPY --from=final /tmp/bins /tmp/bins diff --git a/nix/docker/README.md b/nix/docker/README.md new file mode 100644 index 0000000000..085433b758 --- /dev/null +++ b/nix/docker/README.md @@ -0,0 +1,90 @@ +# Nix CI Docker images + +This directory builds the Docker images used by xrpld's Linux CI. Each image +bundles the **exact same toolchain that the Nix development shell provides** +(see [`docs/build/nix.md`](../../docs/build/nix.md)), so what runs in CI matches +what developers get locally from `nix develop`. + +The toolchain (CMake, Ninja, Conan, GCC, Clang, clang-tidy, the +sanitizer/coverage tools, …) is defined in [`nix/packages.nix`](../packages.nix) +and assembled for CI by [`nix/ci-env.nix`](../ci-env.nix). The Docker build +turns that Nix environment into an ordinary container image layered on top of a +conventional base image (Ubuntu, Debian, RHEL, or `nixos/nix`). + +## Images + +The images are built by the [`build-nix-images.yml`](../../.github/workflows/build-nix-images.yml) +workflow and pushed to `ghcr.io/xrplf/xrpld/nix-`. The `` is +selected through the `BASE_IMAGE` build argument; the base images are the +**oldest supported version** of each distribution we target: + +| Image | `BASE_IMAGE` | Notes | +| ------------ | -------------------------------------------- | -------------------------------------------------- | +| `nix-nixos` | `nixos/nix:latest` | Build/lint only; binaries are not run (see below). | +| `nix-ubuntu` | `ubuntu:20.04` | Oldest supported Ubuntu (glibc 2.31). | +| `nix-debian` | `debian:bookworm` | | +| `nix-rhel` | `registry.access.redhat.com/ubi9/ubi:latest` | | + +All images carry the full toolchain on `PATH` (via `/nix/ci-env/bin`) plus the +CA bundle shipped in the Nix environment, so HTTPS clients (git, curl, Conan) +work without `ca-certificates` being installed in the base image. + +## Build stages + +[`Dockerfile`](./Dockerfile) is a multi-stage build: + +1. **`builder`** — On a `nixos/nix` builder, evaluate the flake and build the + CI environment (`nix/ci-env.nix`). The resulting Nix store closure (the + complete set of store paths the toolchain depends on) is copied into a + staging directory. +2. **`final`** — Start from `BASE_IMAGE`, copy in the Nix store closure and the + `ci-env` symlink tree, and wire up `PATH` and the CA bundle. It then: + - installs the dynamic linker if the base image lacks one (see + [How libc is handled](#how-libc-is-handled)), + - runs [`bin/check-tools.sh`](../../bin/check-tools.sh) to verify every + expected tool is present and runnable, and + - compiles the C++ test programs in + [`test_files/`](./test_files) with both `g++` and `clang++`, and sanitizers. +3. **`tester`** — Start again from a clean `BASE_IMAGE` (no Nix toolchain), + install only the sanitizer runtime libraries + ([`install-sanitizer-libs.sh`](./install-sanitizer-libs.sh)), and run the + binaries compiled in `final`. This proves the binaries built with the Nix + toolchain actually run on a vanilla base image. On `nixos/nix` this step is + skipped (the binaries are patched for a conventional FHS loader). +4. **Output** — The final image is gated on the tester succeeding: it copies a + sentinel file out of `tester`, so a failed test run fails the whole build. + +## How libc is handled + +The goal is for binaries built in these images to run on the **oldest supported +base image** (Ubuntu 20.04, glibc 2.31) and newer — without the developer's Nix +toolchain being present at runtime. Two pieces make that work: + +- **Compilers linked against an old glibc.** The Nix CI environment does not use + nixpkgs' current glibc. Instead it pins a 2020 nixpkgs snapshot whose primary + glibc is **2.31** (matching Ubuntu 20.04), via the `nixpkgs-custom-glibc` + flake input. GCC, Clang, binutils and compiler-rt are all rebuilt/wrapped + against this custom glibc (see [`nix/ci-env.nix`](../ci-env.nix)). As a result + the libraries they emit (`libstdc++`, `libgcc_s`, the sanitizer runtimes) + reference only symbols available in glibc 2.31. + +- **An expected dynamic linker in the image.** + Binaries built in Nix environments reference a dynamic linker from Nix store paths, which won't be present in the base image. However, + [`loader-path.sh`](./loader-path.sh) reports the expected loader path for the + current architecture, so we can patch the binaries to use the correct loader. + +The build then verifies all of this end to end: the test programs in +`test_files/` (a regular binary plus ASan/TSan/UBSan variants) are compiled in +`final`, their `PT_INTERP` is patched to the target loader, and they are run in +the clean `tester` stage to confirm each emits the expected sanitizer +diagnostic on a stock base image. + +## Files + +| File | Purpose | +| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. | +| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. | +| [`./test_files/`](./test_files) | C++ sources and scripts to compile and run the sanitizer smoke tests. | +| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. | +| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. | diff --git a/nix/docker/check-tools.sh b/nix/docker/check-tools.sh deleted file mode 100755 index a46c2dd997..0000000000 --- a/nix/docker/check-tools.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Verify that every tool expected in the Nix CI env is present and runnable. -set -euo pipefail - -ccache --version -clang --version -clang++ --version -clang-format --version -ClangBuildAnalyzer --version -cmake --version -conan --version -curl --version -doxygen --version -file --version -g++ --version -gcc --version -gcov --version -gcovr --version -gh --version -git --version -git-cliff --version -gpg --version -less --version -make --version -mold --version -netstat --version -ninja --version -perl --version -pkg-config --version -pre-commit --version -python3 --version -run-clang-tidy --help -vim --version - -# A simple test to verify that git can clone a repository over HTTPS -# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up. -tmp_clone="$(mktemp -d)" -git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions" -rm -rf "${tmp_clone}" diff --git a/nix/packages.nix b/nix/packages.nix index 5a7f20ec49..6202168733 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -19,8 +19,10 @@ in gh git git-cliff + git-lfs gnumake gnupg # needed for signing commits & codecov/codecov-action + graphviz llvmPackages_22.clang-tools less # needed for git diff mold @@ -33,5 +35,6 @@ in python3 runClangTidy vim + zip ]; } diff --git a/package/README.md b/package/README.md index 9a1f76c597..63c2ab88fc 100644 --- a/package/README.md +++ b/package/README.md @@ -15,7 +15,6 @@ package/ xrpld.sysusers sysusers.d config (used by both RPM and DEB) xrpld.tmpfiles tmpfiles.d config (used by both RPM and DEB) xrpld.logrotate logrotate config (installed to /etc/logrotate.d/xrpld) - update-xrpld auto-update script (installed to /usr/libexec/xrpld/, run by update-xrpld.timer) ``` ## Prerequisites diff --git a/package/build_pkg.sh b/package/build_pkg.sh index f2c2c63c12..e2ec8fee3d 100755 --- a/package/build_pkg.sh +++ b/package/build_pkg.sh @@ -114,10 +114,11 @@ VER_BASE="${VERSION%%-*}" VER_SUFFIX="${VERSION#*-}" [[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX="" -# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). The RPM -# Release field forbids '-', and the convention here is single-token suffixes -# like b1 or rc2. Fail early with a clear message rather than letting either -# rpmbuild blow up or silently mangling dashes into dots. +# Reject multi-segment suffixes (e.g. "beta-1", "rc1-15-gabc123"). Neither an +# RPM Version nor a Debian upstream version may contain '-' (it's the NVR / +# version-revision separator), and the convention here is single-token +# suffixes like b1 or rc2. Fail early with a clear message rather than letting +# the package tooling blow up or silently mangle dashes. if [[ "${VER_SUFFIX}" == *-* ]]; then echo "build_pkg.sh: multi-segment pre-release in VERSION='${VERSION}' (suffix '${VER_SUFFIX}')." >&2 echo "Use single-token suffixes like 3.2.0-b1 or 3.2.0-rc2." >&2 @@ -142,9 +143,6 @@ stage_common() { cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers" cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles" cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate" - cp "${SHARED}/update-xrpld" "${dest}/update-xrpld" - cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service" - cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer" cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset" } @@ -156,20 +154,18 @@ build_rpm() { cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec" stage_common "${topdir}/SOURCES" - # RPM Version can't contain '-'. A pre-release goes in Release with a - # leading "0." so 3.2.0-b1 sorts before the final 3.2.0-. - # The order is "0.." (e.g. 0.1.b6) — the Fedora/EPEL - # convention. Reversing to "0.." (e.g. 0.b6.1) breaks - # rpmvercmp against the former because numeric segments outrank alphabetic - # ones, so "0.1.b5" would sort newer than "0.b6.1". - local rpm_release="${PKG_RELEASE}" - [[ -n "${VER_SUFFIX}" ]] && rpm_release="0.${PKG_RELEASE}.${VER_SUFFIX}" + # Pre-releases use the modern rpm '~' convention (rpm >= 4.10): the suffix + # goes in Version (e.g. 3.2.0~b1), which rpmvercmp sorts *before* the final + # 3.2.0 — identical semantics to Debian's '~'. Release is just the package + # release number. This replaces the older "0.." Release + # hack and keeps the RPM and DEB version strings symmetric. + local rpm_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}" set -x rpmbuild -bb \ --define "_topdir ${topdir}" \ - --define "xrpld_version ${VER_BASE}" \ - --define "xrpld_release ${rpm_release}" \ + --define "xrpld_version ${rpm_version}" \ + --define "xrpld_release ${PKG_RELEASE}" \ "${topdir}/SPECS/xrpld.spec" } @@ -181,13 +177,10 @@ build_deb() { stage_common "${staging}" cp -r "${DEBIAN_DIR}" "${staging}/debian" - # Debhelper auto-discovers these only from debian/. cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service" cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers" cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles" cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate" - cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service" - cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer" # Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0. local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}" diff --git a/package/debian/rules b/package/debian/rules index 612fe1b1a9..16574bca3f 100644 --- a/package/debian/rules +++ b/package/debian/rules @@ -10,7 +10,6 @@ override_dh_auto_configure override_dh_auto_build override_dh_auto_test: override_dh_installsystemd: dh_installsystemd --no-stop-on-upgrade xrpld.service - dh_installsystemd --name=update-xrpld --no-enable --no-start update-xrpld.service update-xrpld.timer execute_before_dh_installtmpfiles: dh_installsysusers @@ -21,7 +20,6 @@ override_dh_install: install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt - install -D -m 0755 update-xrpld debian/xrpld/usr/libexec/xrpld/update-xrpld override_dh_dwz: @: diff --git a/package/debian/xrpld.docs b/package/debian/xrpld.docs index 1217b6db43..b43bf86b50 100644 --- a/package/debian/xrpld.docs +++ b/package/debian/xrpld.docs @@ -1,2 +1 @@ README.md -LICENSE.md diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec index 4933c724f7..5595fd0d8d 100644 --- a/package/rpm/xrpld.spec +++ b/package/rpm/xrpld.spec @@ -35,8 +35,6 @@ install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{ # systemd units, sysusers, tmpfiles, preset install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service -install -Dm0644 %{_sourcedir}/update-xrpld.service %{buildroot}%{_unitdir}/update-xrpld.service -install -Dm0644 %{_sourcedir}/update-xrpld.timer %{buildroot}%{_unitdir}/update-xrpld.timer install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50-xrpld.preset @@ -44,9 +42,6 @@ install -Dm0644 %{_sourcedir}/50-xrpld.preset %{buildroot}%{_presetdir}/50- # Logrotate config install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name} -# Update helper -install -Dm0755 %{_sourcedir}/update-xrpld %{buildroot}%{_libexecdir}/%{name}/update-xrpld - # Docs install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md install -Dm0644 %{_sourcedir}/README.md %{buildroot}%{_docdir}/%{name}/README.md @@ -61,10 +56,10 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled %post systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : -%systemd_post xrpld.service update-xrpld.timer +%systemd_post xrpld.service %preun -%systemd_preun xrpld.service update-xrpld.timer +%systemd_preun xrpld.service %postun %systemd_postun_with_restart xrpld.service @@ -74,7 +69,6 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %doc %{_docdir}/%{name}/README.md %dir %{_sysconfdir}/%{name} -%dir %{_libexecdir}/%{name} %{_bindir}/%{name} @@ -82,18 +76,13 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %config(noreplace) %{_sysconfdir}/%{name}/validators.txt %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} -%{_libexecdir}/%{name}/update-xrpld %{_unitdir}/xrpld.service -%{_unitdir}/update-xrpld.service -%{_unitdir}/update-xrpld.timer %{_presetdir}/50-xrpld.preset %{_sysusersdir}/xrpld.conf %{_tmpfilesdir}/xrpld.conf - -%ghost %dir /var/lib/%{name} -%ghost %dir /var/log/%{name} - +%ghost %dir /var/lib/xrpld +%ghost %dir /var/log/xrpld # Legacy compatibility for pre-FHS package layouts. # TODO: remove after rippled fully deprecated. diff --git a/package/shared/50-xrpld.preset b/package/shared/50-xrpld.preset index 6264e00131..bfbcd56577 100644 --- a/package/shared/50-xrpld.preset +++ b/package/shared/50-xrpld.preset @@ -1,4 +1,2 @@ # /usr/lib/systemd/system-preset/50-xrpld.preset enable xrpld.service -# Don't enable automatic updates -disable update-xrpld.timer diff --git a/package/shared/update-xrpld b/package/shared/update-xrpld deleted file mode 100755 index 4bd4db2538..0000000000 --- a/package/shared/update-xrpld +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Optional: also write logs to a legacy file in addition to journald. -# By default, this script logs to systemd/journald, viewable via: -# journalctl -t update-xrpld -# -# Uncomment the line below if you need a flat file for compatibility with -# external tooling, manual inspection, or environments where journald logs -# are not persisted or easily accessible. -# -# Note: This duplicates all output (stdout/stderr) to both journald and the file. -# It is generally not needed on modern systems and may cause log file growth -# if left enabled long-term. -# -# Requires /var/log/xrpld/ to exist and be writable by the service (root). -# -# exec > >(tee -a /var/log/xrpld/update.log) 2>&1 - -PATH=/usr/sbin:/usr/bin:/sbin:/bin - -PKG_NAME=${PKG_NAME:-xrpld} - -log() { - # If running under systemd/journald, let it handle timestamps. - if [[ -n "${JOURNAL_STREAM:-}" ]]; then - printf '%s\n' "$*" - else - printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" - fi -} - -require_root() { - if [[ ${EUID:-$(id -u)} -ne 0 ]]; then - log "RESULT: failed reason=not-root" - exit 1 - fi -} - -get_installed_version() { - if command -v dpkg-query >/dev/null 2>&1; then - dpkg-query -W -f='${Version}' "$PKG_NAME" 2>/dev/null || printf 'unknown' - elif command -v rpm >/dev/null 2>&1; then - rpm -q --qf '%{VERSION}-%{RELEASE}' "$PKG_NAME" 2>/dev/null || printf 'unknown' - else - printf 'unknown' - fi -} - -trap 'log "RESULT: failed reason=script-error exit_code=$?"' ERR - -apt_can_update() { - apt-get update -qq - apt-get -s --only-upgrade install "$PKG_NAME" 2>/dev/null | grep -q "^Inst ${PKG_NAME}\b" -} - -apt_apply_update() { - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ - -o Dpkg::Options::="--force-confdef" \ - -o Dpkg::Options::="--force-confold" \ - "$PKG_NAME" -} - -get_rpm_pm() { - if command -v dnf >/dev/null 2>&1; then - printf 'dnf\n' - elif command -v yum >/dev/null 2>&1; then - printf 'yum\n' - else - return 1 - fi -} - -rpm_refresh_metadata() { - local pm=$1 - if [[ "$pm" == "dnf" ]]; then - dnf makecache --refresh -q >/dev/null - else - yum clean expire-cache -q >/dev/null - fi -} - -rpm_can_update() { - local pm=$1 - - rpm_refresh_metadata "$pm" - local rc=0 - set +e - "$pm" check-update -q "$PKG_NAME" >/dev/null 2>&1 - rc=$? - set -e - - if [[ $rc -eq 100 ]]; then - return 0 - elif [[ $rc -eq 0 ]]; then - return 1 - else - log "$pm check-update failed with exit code ${rc}." - exit 1 - fi -} - -rpm_apply_update() { - local pm=$1 - "$pm" update -y "$PKG_NAME" -} - -restart_service() { - # Preserve the operator's prior service state: if xrpld was intentionally - # stopped before the update, don't bring it back up just because the - # auto-update timer fired. - if systemctl is-active --quiet "${PKG_NAME}.service"; then - systemctl restart "${PKG_NAME}.service" - log "${PKG_NAME} service restarted successfully." - else - log "${PKG_NAME} service was not running; skipping restart to preserve prior state." - fi -} - -main() { - require_root - if command -v apt-get >/dev/null 2>&1; then - log "Checking for ${PKG_NAME} updates via apt" - if apt_can_update; then - log "Update available; installing." - apt_apply_update - restart_service - log "RESULT: updated ${PKG_NAME}=$(get_installed_version)" - else - log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)" - fi - return - fi - - local rpm_pm="" - if rpm_pm="$(get_rpm_pm)"; then - log "Checking for ${PKG_NAME} updates via ${rpm_pm}" - if rpm_can_update "$rpm_pm"; then - log "Update available; installing" - rpm_apply_update "$rpm_pm" - restart_service - log "RESULT: updated ${PKG_NAME}=$(get_installed_version)" - else - log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)" - fi - return - fi - log "RESULT: failed reason=no-package-manager" - exit 1 -} - -main "$@" diff --git a/package/shared/update-xrpld.service b/package/shared/update-xrpld.service deleted file mode 100644 index a964ca5482..0000000000 --- a/package/shared/update-xrpld.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Check for and install xrpld package updates -Documentation=man:systemd.service(5) -Wants=network-online.target -After=network-online.target -ConditionPathExists=/usr/libexec/xrpld/update-xrpld -ConditionPathExists=/usr/bin/xrpld - -[Service] -Type=oneshot -ExecStart=/usr/bin/flock -n /run/lock/xrpld-update.lock /usr/libexec/xrpld/update-xrpld -StandardOutput=journal -StandardError=journal -SyslogIdentifier=update-xrpld -TimeoutStartSec=30min -PrivateTmp=true diff --git a/package/shared/update-xrpld.timer b/package/shared/update-xrpld.timer deleted file mode 100644 index 9fba09d30a..0000000000 --- a/package/shared/update-xrpld.timer +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Daily xrpld update check - -[Timer] -OnCalendar=*-*-* 00:00:00 -RandomizedDelaySec=4h -Persistent=true - -[Install] -WantedBy=timers.target diff --git a/package/shared/xrpld.service b/package/shared/xrpld.service index 8e10ed2eee..f54e47aa14 100644 --- a/package/shared/xrpld.service +++ b/package/shared/xrpld.service @@ -17,7 +17,16 @@ ProtectHome=true PrivateTmp=true User=xrpld Group=xrpld +StateDirectory=xrpld +StateDirectoryMode=0750 +LogsDirectory=xrpld +LogsDirectoryMode=0750 LimitNOFILE=65536 +SystemCallArchitectures=native + +# Uncomment both lines to allow xrpld to bind to privileged ports (<1024) +#CapabilityBoundingSet=CAP_NET_BIND_SERVICE +#AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target diff --git a/src/libxrpl/ledger/BookListeners.cpp b/src/libxrpl/ledger/BookListeners.cpp deleted file mode 100644 index d78da4c73e..0000000000 --- a/src/libxrpl/ledger/BookListeners.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include - -#include -#include -#include -#include - -#include -#include - -namespace xrpl { - -void -BookListeners::addSubscriber(InfoSub::ref sub) -{ - std::scoped_lock const sl(lock_); - listeners_[sub->getSeq()] = sub; -} - -void -BookListeners::removeSubscriber(std::uint64_t seq) -{ - std::scoped_lock const sl(lock_); - listeners_.erase(seq); -} - -void -BookListeners::publish(MultiApiJson const& jvObj, hash_set& havePublished) -{ - std::scoped_lock const sl(lock_); - auto it = listeners_.cbegin(); - - while (it != listeners_.cend()) - { - InfoSub::pointer p = it->second.lock(); - - if (p) - { - // Only publish jvObj if this is the first occurrence - if (havePublished.emplace(p->getSeq()).second) - { - jvObj.visit( - p->getApiVersion(), // - [&](json::Value const& jv) { p->send(jv, true); }); - } - ++it; - } - else - { - it = listeners_.erase(it); - } - } -} - -} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp index a59b8e4436..cacbfc9d58 100644 --- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp @@ -433,6 +433,43 @@ ammPoolHolds( return std::make_pair(assetInBalance, assetOutBalance); } +TER +checkAMMPrecisionLoss(Number const& poolProductMean, STAmount const& newLPTokenBalance) +{ + if (newLPTokenBalance <= beast::kZero) + return tesSUCCESS; + if (poolProductMean >= newLPTokenBalance) + return tesSUCCESS; + // Strong check failed. Allow the same relative tolerance as the invariant + // checker's weak check. Only return tecPRECISION_LOSS when both fail. + if (withinRelativeDistance( + poolProductMean, Number{newLPTokenBalance}, kAMMInvariantRelativeTolerance)) + return tesSUCCESS; + return tecPRECISION_LOSS; +} + +TER +checkAMMPrecisionLoss( + ReadView const& view, + AccountID const& ammAccountID, + Asset const& asset1, + Asset const& asset2, + STAmount const& newLPTokenBalance, + beast::Journal const j) +{ + if (newLPTokenBalance <= beast::kZero) + return tesSUCCESS; + auto const [amount, amount2] = ammPoolHolds( + view, + ammAccountID, + asset1, + asset2, + FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, + j); + return checkAMMPrecisionLoss(root2(amount * amount2), newLPTokenBalance); +} + std::expected, TER> ammHolds( ReadView const& view, diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 2d0573f36e..3264fa1a45 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -51,141 +51,103 @@ isGlobalFrozen(ReadView const& view, AccountID const& issuer) // Returns adjusted owner count. static std::uint32_t confineOwnerCount( - std::uint32_t current, - std::int32_t adjustment, + std::uint32_t currentOwnerCount, + std::int32_t ownerCountAdj, std::optional const& id = std::nullopt, beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) { - std::uint32_t adjusted{current + adjustment}; - if (adjustment > 0) + std::uint32_t totalOwnerCount{currentOwnerCount + ownerCountAdj}; + if (ownerCountAdj > 0) { // Overflow is well defined on unsigned - if (adjusted < current) + if (totalOwnerCount < currentOwnerCount) { if (id) { JLOG(j.fatal()) << "Account " << *id << " owner count exceeds max!"; } - adjusted = std::numeric_limits::max(); + totalOwnerCount = std::numeric_limits::max(); } } else { // Underflow is well defined on unsigned - if (adjusted > current) + if (totalOwnerCount > currentOwnerCount) { if (id) { JLOG(j.fatal()) << "Account " << *id << " owner count set below 0!"; } - adjusted = 0; + totalOwnerCount = 0; XRPL_ASSERT(!id, "xrpl::confineOwnerCount : id is not set"); } } - return adjusted; + return totalOwnerCount; } +// Return number of the accounts which reserve is covered by current account (so called "reserve +// count") static std::uint32_t -ownerCountHlp( - ReadView const& view, - SLE::const_ref sle, - std::int32_t adjustment, - bool reportConfine, - beast::Journal j) +accountCountImpl(SLE::const_ref sle, std::int32_t accountCountAdj, beast::Journal j) { - AccountID const id = sle->getAccountID(sfAccount); - std::uint32_t const savedCount = sle->at(sfOwnerCount); - std::uint32_t const hookedCount = view.ownerCountHook(id, savedCount); + bool const isSponsored = sle->isFieldPresent(sfSponsor); + std::int64_t const sponsoringAccountCount = sle->getFieldU32(sfSponsoringAccountCount); + std::int64_t const currentAccountCount = (isSponsored ? 0 : 1) + sponsoringAccountCount; - std::uint32_t const sponsoredCount = sle->at(sfSponsoredOwnerCount); - std::uint32_t const sponsoringCount = sle->at(sfSponsoringOwnerCount); - - if (hookedCount < sponsoredCount) + std::int64_t totalAccountCount{currentAccountCount + accountCountAdj}; + if (totalAccountCount > std::numeric_limits::max()) { - Throw( - "xrpl::ownerCountHlp : OwnerCount must be greater than or equal to " - "SponsoredOwnerCount"); + JLOG(j.error()) << "Reserve count exceeds max!"; + totalAccountCount = std::numeric_limits::max(); + } + else if (totalAccountCount < 0) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::accountCountImpl : Reserve count set below 0"); + JLOG(j.fatal()) << "Reserve count set below 0"; + totalAccountCount = 0; + // LCOV_EXCL_STOP } + return totalAccountCount; +} + +std::uint32_t +ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj) +{ + XRPL_ASSERT(sle && sle->getType() == ltACCOUNT_ROOT, "xrpl::ownerCount : sle is account root"); + + AccountID const id = sle->getAccountID(sfAccount); + std::uint32_t const currentOwnerCount = sle->at(sfOwnerCount); + std::uint32_t const sponsoredOwnerCount = sle->at(sfSponsoredOwnerCount); + std::uint32_t const sponsoringOwnerCount = sle->at(sfSponsoringOwnerCount); + + XRPL_ASSERT( + currentOwnerCount >= sponsoredOwnerCount, + "xrpl::ownerCount : OwnerCount must be greater than or equal to " + "SponsoredOwnerCount"); + std::int64_t deltaCount = - static_cast(adjustment) - sponsoredCount + sponsoringCount; + static_cast(ownerCountAdj) - sponsoredOwnerCount + sponsoringOwnerCount; + if (deltaCount > std::numeric_limits::max()) { deltaCount = std::numeric_limits::max(); JLOG(j.fatal()) << "Account " << id << " delta count exceeds max, " - << "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount - << ", sponsoringOwnerCount: " << sponsoringCount; + << "adjustment: " << ownerCountAdj + << ", sponsoredCount: " << sponsoredOwnerCount + << ", sponsoringOwnerCount: " << sponsoringOwnerCount; } else if (deltaCount < std::numeric_limits::min()) { deltaCount = std::numeric_limits::min(); - JLOG(j.fatal()) << "Account " << id << " delta count exceeds min, " - << "adjustment: " << adjustment << ", sponsoredCount: " << sponsoredCount - << ", sponsoringCount: " << sponsoringCount; + JLOG(j.fatal()) << "Account " << id << " delta count is below min, " + << "adjustment: " << ownerCountAdj + << ", sponsoredCount: " << sponsoredOwnerCount + << ", sponsoringCount: " << sponsoringOwnerCount; } - std::uint32_t const confinedCount = reportConfine - ? confineOwnerCount(hookedCount, deltaCount, id, j) - : confineOwnerCount(hookedCount, deltaCount); - - return confinedCount; -} - -static std::uint32_t -reserveCountHlp(SLE::const_ref sle, std::int32_t adjustment, beast::Journal j) -{ - bool const isSponsored = sle->isFieldPresent(sfSponsor); - std::uint32_t const sponsoringCount = sle->getFieldU32(sfSponsoringAccountCount); - std::uint32_t const reserveCount = (isSponsored ? 0 : 1) + sponsoringCount; - - std::uint32_t adjusted{reserveCount + adjustment}; - if (adjustment > 0) - { - // Overflow is well defined on unsigned - if (adjusted < reserveCount) - { - JLOG(j.fatal()) << "Reserve count exceeds max!"; - adjusted = std::numeric_limits::max(); - } - } - else - { - // Underflow is well defined on unsigned - if (adjusted > reserveCount) - { - JLOG(j.fatal()) << "Reserve count set below 0!"; - adjusted = 0; - } - } - return adjusted; -} - -static inline XRPAmount -baseReserveHlp(ReadView const& view, std::uint32_t ownerCount, std::uint32_t reserveCount) -{ - auto const& fees = view.fees(); - return (fees.reserve * reserveCount) + (fees.increment * ownerCount); -} - -static XRPAmount -reserveHlp( - ReadView const& view, - SLE::const_ref sle, - std::uint32_t ownerCount, - std::uint32_t reserveCount) -{ - // Pseudo-accounts have no reserve requirement - if (isPseudoAccount(sle)) - return XRPAmount(0); - - auto const reserve = baseReserveHlp(view, ownerCount, reserveCount); - return reserve; -} - -std::uint32_t -ownerCount(ReadView const& view, SLE::const_ref sle, beast::Journal j, std::int32_t adjustment) -{ - return ownerCountHlp(view, sle, adjustment, true, j); + return confineOwnerCount(currentOwnerCount, deltaCount); } XRPAmount @@ -195,9 +157,15 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, if (sle == nullptr) return beast::kZero; - std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, false, j); - std::uint32_t const reserveCount = reserveCountHlp(sle, 0, j); - auto const reserve = reserveHlp(view, sle, ownerCount, reserveCount); + // Return balance minus reserve + std::uint32_t const currentOwnerCount = + confineOwnerCount(view.ownerCountHook(id, ownerCount(sle, j)), ownerCountAdj); + std::uint32_t const currentAccountCount = accountCountImpl(sle, 0, j); + + // Pseudo-accounts have no reserve requirement + auto const reserve = isPseudoAccount(sle) + ? XRPAmount{0} + : baseAccountReserve(view, currentOwnerCount, currentAccountCount); auto const fullBalance = sle->getFieldAmount(sfBalance); @@ -209,7 +177,7 @@ xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, << " amount=" << amount.getFullText() << " fullBalance=" << fullBalance.getFullText() << " balance=" << balance.getFullText() << " reserve=" << reserve - << " ownerCount=" << ownerCount << " ownerCountAdj=" << ownerCountAdj; + << " ownerCount=" << currentOwnerCount << " ownerCountAdj=" << ownerCountAdj; return amount.xrp(); } @@ -226,20 +194,21 @@ transferRate(ReadView const& view, AccountID const& issuer) } static void -adjustOwnerCountHlp( +adjustOwnerCountImpl( ApplyView& view, SLE::ref sle, SF_UINT32 const& sfield, AccountID const& accID, - std::int32_t adjustment, + std::int32_t ownerCountAdj, beast::Journal j, bool callHook = true) { - std::uint32_t const current = sle->at(sfield); - std::uint32_t const adjusted = confineOwnerCount(current, adjustment, accID, j); + std::uint32_t const currentOwnerCount = sle->at(sfield); + std::uint32_t const totalOwnerCount = + confineOwnerCount(currentOwnerCount, ownerCountAdj, accID, j); if (callHook) - view.adjustOwnerCountHook(accID, current, adjusted); - sle->at(sfield) = adjusted; + view.adjustOwnerCountHook(accID, currentOwnerCount, totalOwnerCount); + sle->at(sfield) = totalOwnerCount; view.update(sle); } @@ -248,7 +217,7 @@ adjustOwnerCount( ApplyView& view, SLE::ref accountSle, SLE::ref sponsorSle, - std::int32_t adjustment, + std::int32_t ownerCountAdj, beast::Journal j) { if (!accountSle) @@ -260,8 +229,8 @@ adjustOwnerCount( if (!validType) Throw("xrpl::adjustOwnerCount : valid account sle type"); - XRPL_ASSERT(adjustment, "xrpl::adjustOwnerCount : nonzero adjustment input"); - if (adjustment == 0) + XRPL_ASSERT(ownerCountAdj, "xrpl::adjustOwnerCount : nonzero ownerCountAdj input"); + if (ownerCountAdj == 0) return; auto const accountID = accountSle->getAccountID(sfAccount); @@ -271,19 +240,21 @@ adjustOwnerCount( Throw("xrpl::adjustOwnerCount : valid sponsor sle type"); auto const sponsorID = sponsorSle->getAccountID(sfAccount); - adjustOwnerCountHlp(view, accountSle, sfSponsoredOwnerCount, accountID, adjustment, j); - adjustOwnerCountHlp(view, sponsorSle, sfSponsoringOwnerCount, sponsorID, adjustment, j); + adjustOwnerCountImpl(view, accountSle, sfSponsoredOwnerCount, accountID, ownerCountAdj, j); + adjustOwnerCountImpl(view, sponsorSle, sfSponsoringOwnerCount, sponsorID, ownerCountAdj, j); - auto sponsorObjSle = view.peek(keylet::sponsor(sponsorID, accountID)); - if (sponsorObjSle && adjustment > 0) + auto sponsorshipSle = view.peek(keylet::sponsorship(sponsorID, accountID)); + if (sponsorshipSle && ownerCountAdj > 0) { - // update the pre-funded ReserveCount on Sponsorship ledger object - // Reserve count moves opposite to adjustment: +adjustment => consume reserve (-), - adjustOwnerCountHlp( - view, sponsorObjSle, sfReserveCount, sponsorID, -adjustment, j, false); + // Only decrease the pre-funded ReserveCount on Sponsorship if we assign new objects. + // Removing/reassigning ownership of the object doesn't increase RemainingOwnerCount + // back. Don't call hook because this counter is not something that require reserve + // (like other sf...OwnerCounts do). + adjustOwnerCountImpl( + view, sponsorshipSle, sfRemainingOwnerCount, sponsorID, -ownerCountAdj, j, false); } } - adjustOwnerCountHlp(view, accountSle, sfOwnerCount, accountID, adjustment, j); + adjustOwnerCountImpl(view, accountSle, sfOwnerCount, accountID, ownerCountAdj, j); } void @@ -291,7 +262,7 @@ adjustOwnerCountObj( ApplyView& view, SLE::ref accountSle, SLE::ref objectSle, - std::int32_t amount, + std::int32_t accountCountAdj, beast::Journal j) { if (!objectSle) @@ -300,7 +271,7 @@ adjustOwnerCountObj( Throw("xrpl::adjustOwnerCount : valid object sle type"); SLE::ref sponsorSle = getLedgerEntryReserveSponsor(view, objectSle); - adjustOwnerCount(view, accountSle, sponsorSle, amount, j); + adjustOwnerCount(view, accountSle, sponsorSle, accountCountAdj, j); } XRPAmount @@ -309,24 +280,24 @@ accountReserve( SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj, - std::int32_t reserveCountAdj) + std::int32_t accountCountAdj) { if (!sle) Throw("xrpl::accountReserve : valid sle"); if (sle->getType() != ltACCOUNT_ROOT) Throw("xrpl::accountReserve : valid sle type"); - std::uint32_t const ownerCount = ownerCountHlp(view, sle, ownerCountAdj, true, j); - std::uint32_t const reserveCount = reserveCountHlp(sle, reserveCountAdj, j); + std::uint32_t const currentOwnerCount = ownerCount(sle, j, ownerCountAdj); + std::uint32_t const currentAccountCount = accountCountImpl(sle, accountCountAdj, j); - return reserveHlp(view, sle, ownerCount, reserveCount); + return baseAccountReserve(view, currentOwnerCount, currentAccountCount); } XRPAmount -baseAccountReserve(ReadView const& view, std::int32_t ownerCount) +baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t accountCount) { - auto const reserve = baseReserveHlp(view, ownerCount, 1); - return reserve; + auto const& fees = view.fees(); + return (fees.reserve * accountCount) + (fees.increment * ownerCount); } TER @@ -336,39 +307,39 @@ checkInsufficientReserve( SLE::const_ref accSle, STAmount const& accBalance, SLE::const_ref sponsorSle, - std::int32_t ownerCountDelta, - std::int32_t reserveCountDelta, + std::int32_t ownerCountAdj, + std::int32_t accountCountAdj, beast::Journal j) { if (sponsorSle) { - auto const isCoSigning = isSponsorReserveCoSigning(tx); - auto const sle = view.read( - keylet::sponsor(sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); + keylet::sponsorship( + sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); - // prefunded sponsor should have a sponsorship entry - if (!isCoSigning && !sle) + // A reserve-sponsored tx must carry a sponsor signature + // (cosigning path) and/or have a pre-existing sponsorship SLE + // (prefunded path). Absence of both is an internal invariant break. + if (isReserveSponsored(tx) && !sle && !tx.isFieldPresent(sfSponsorSignature)) return tecINTERNAL; // LCOV_EXCL_LINE if (sle) { - auto const ownerCountAllowed = sle->getFieldU32(sfReserveCount); - if (ownerCountAllowed < ownerCountDelta) + auto const ownerCountAllowed = sle->getFieldU32(sfRemainingOwnerCount); + if (ownerCountAllowed < ownerCountAdj) return tecINSUFFICIENT_RESERVE; } auto const sponsorBalance = sponsorSle->getFieldAmount(sfBalance); STAmount const sponsorReserve = - accountReserve(view, sponsorSle, j, ownerCountDelta, reserveCountDelta); + accountReserve(view, sponsorSle, j, ownerCountAdj, accountCountAdj); if (sponsorBalance < sponsorReserve) return tecINSUFFICIENT_RESERVE; } else { - STAmount const reserve = - accountReserve(view, accSle, j, ownerCountDelta, reserveCountDelta); + STAmount const reserve = accountReserve(view, accSle, j, ownerCountAdj, accountCountAdj); if (accBalance < reserve) return tecINSUFFICIENT_RESERVE; } diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index 1f6faeef86..7638558bbf 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -97,7 +97,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } if (isOwner) - adjustOwnerCountObj(view, sleAccount, sleCredential, -1, j); + adjustOwnerCount(view, sleAccount, {}, -1, j); return tesSUCCESS; }; diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 23269f710a..c98f2c7cd5 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -35,6 +35,7 @@ #include #include #include +#include namespace xrpl { @@ -144,7 +145,7 @@ addEmptyHolding( if (accountID == mptIssue.getIssuer()) return tesSUCCESS; - return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal); + return authorizeMPToken(view, tx, priorBalance, mptID, accountID, journal, 0, std::nullopt); } [[nodiscard]] TER @@ -193,24 +194,27 @@ authorizeMPToken( // - add the new mptokenKey to the owner directory // - create the MPToken object for the holder - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - auto const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(tx); + SLE::pointer sponsorSle; + if (account == tx[sfAccount]) + { + auto sle = getTxReserveSponsor(view, tx); + if (!sle) + return sle.error(); // LCOV_EXCL_LINE + sponsorSle = std::move(*sle); + } // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item // an account owns, in the case of MPTokens we only // *enforce* a reserve if the user owns more than two // items. This is similar to the reserve requirements of trust lines. - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. - if (ownerCount(view, *sponsorSle ? *sponsorSle : sleAcct, journal) >= 2 || - isSponsoredAndPreFunded) + // The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on + // the tx — the sponsor must always cover the reserve (via balance or prefunded + // budget), so this check always runs for sponsored transactions. + if (sponsorSle || ownerCount(sleAcct, journal) >= 2) { if (auto const ret = checkInsufficientReserve( - view, tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal); + view, tx, sleAcct, priorBalance, sponsorSle, 1, 0, journal); !isTesSuccess(ret)) return ret; } @@ -237,8 +241,8 @@ authorizeMPToken( view.insert(mptoken); // Update owner count. - adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal); - addSponsorToLedgerEntry(mptoken, *sponsorSle); + adjustOwnerCount(view, sleAcct, sponsorSle, 1, journal); + addSponsorToLedgerEntry(mptoken, sponsorSle); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index c77f49c864..93dbae0467 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -4,14 +4,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include #include @@ -23,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -35,7 +32,6 @@ #include #include #include -#include #include #include #include @@ -71,15 +67,12 @@ locatePage(ApplyView& view, AccountID const& owner, uint256 const& id) Keylet(ltNFTOKEN_PAGE, view.succ(first.key, last.key.next()).value_or(last.key))); } -static std::expected +static SLE::pointer getPageForToken( ApplyView& view, - STTx const& tx, AccountID const& owner, - SLE::ref sponsorSle, uint256 const& id, - std::function const& - createCallback) + std::function const& createCallback) { auto const base = keylet::nftpageMin(owner); auto const first = keylet::nftpage(base, id); @@ -98,9 +91,7 @@ getPageForToken( cp = std::make_shared(last); cp->setFieldArray(sfNFTokens, arr); view.insert(cp); - - if (auto const ret = createCallback(view, tx, cp, owner, sponsorSle); !isTesSuccess(ret)) - return std::unexpected(ret); + createCallback(view, owner); return cp; } @@ -213,8 +204,7 @@ getPageForToken( cp->setFieldH256(sfPreviousPageMin, np->key()); view.update(cp); - if (auto const ret = createCallback(view, tx, np, owner, sponsorSle); ret != tesSUCCESS) - return std::unexpected(ret); + createCallback(view, owner); return (first.key < np->key()) ? np : cp; } @@ -270,55 +260,33 @@ changeTokenURI( /** Insert the token in the owner's token directory. */ TER -insertToken(ApplyView& view, STTx const& tx, AccountID owner, SLE::ref sponsorSle, STObject&& nft) +insertToken(ApplyView& view, AccountID owner, STObject&& nft) { XRPL_ASSERT(nft.isFieldPresent(sfNFTokenID), "xrpl::nft::insertToken : has NFT token"); // First, we need to locate the page the NFT belongs to, creating it // if necessary. This operation may fail if it is impossible to insert // the NFT. - auto createCallback = [](ApplyView& view, - STTx const& tx, - std::shared_ptr const& newPage, - AccountID const& owner, - SLE::ref sponsorSle) -> TER { - if (isReserveSponsored(tx)) - { - auto const ownerSle = view.read(keylet::account(owner)); - auto const ownerBalance = ownerSle->getFieldAmount(sfBalance); - if (auto const ret = - checkInsufficientReserve(view, tx, ownerSle, ownerBalance, sponsorSle, 1); - !isTesSuccess(ret)) - return ret; - } + SLE::pointer const page = + getPageForToken(view, owner, nft[sfNFTokenID], [](ApplyView& view, AccountID const& owner) { + adjustOwnerCount(view, owner, {}, 1, beast::Journal{beast::Journal::getNullSink()}); + }); - adjustOwnerCount(view, view.peek(keylet::account(owner)), sponsorSle, 1); - - addSponsorToLedgerEntry(newPage, sponsorSle); - return tesSUCCESS; - }; - - auto const page = - getPageForToken(view, tx, owner, sponsorSle, nft[sfNFTokenID], createCallback); - - if (!page.has_value()) - return page.error(); - - if (!(*page)) + if (!page) return tecNO_SUITABLE_NFTOKEN_PAGE; { - auto arr = (*page)->getFieldArray(sfNFTokens); + auto arr = page->getFieldArray(sfNFTokens); arr.pushBack(std::move(nft)); arr.sort([](STObject const& o1, STObject const& o2) { return compareTokens(o1.getFieldH256(sfNFTokenID), o2.getFieldH256(sfNFTokenID)); }); - (*page)->setFieldArray(sfNFTokens, arr); + page->setFieldArray(sfNFTokens, arr); } - view.update((*page)); + view.update(page); return tesSUCCESS; } @@ -439,11 +407,18 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->setFieldArray(sfNFTokens, arr); view.update(curr); + int cnt = 0; + if (prev && mergePages(view, prev, curr)) - adjustOwnerCountObj(view, owner, prev, -1); + cnt--; if (next && mergePages(view, curr, next)) - adjustOwnerCountObj(view, owner, curr, -1); + cnt--; + + if (cnt != 0) + { + adjustOwnerCount(view, owner, {}, cnt, beast::Journal{beast::Journal::getNullSink()}); + } return tesSUCCESS; } @@ -477,7 +452,7 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S curr->makeFieldAbsent(sfPreviousPageMin); } - adjustOwnerCountObj(view, owner, prev, -1); + adjustOwnerCount(view, owner, {}, -1, beast::Journal{beast::Journal::getNullSink()}); view.update(curr); view.erase(prev); @@ -513,10 +488,10 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view.update(next); } - adjustOwnerCountObj(view, owner, curr, -1); - view.erase(curr); + int cnt = 1; + // Since we're here, try to consolidate the previous and current pages // of the page we removed (if any) into one. mergePages() _should_ // always return false. Since tokens are burned one at a time, there @@ -530,9 +505,9 @@ removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID, S view, view.peek(Keylet(ltNFTOKEN_PAGE, prev->key())), view.peek(Keylet(ltNFTOKEN_PAGE, next->key())))) - { - adjustOwnerCountObj(view, owner, prev, -1); - } + cnt++; + + adjustOwnerCount(view, owner, {}, -1 * cnt, beast::Journal{beast::Journal::getNullSink()}); return tesSUCCESS; } @@ -648,7 +623,7 @@ deleteTokenOffer(ApplyView& view, SLE::ref offer) false)) return false; - adjustOwnerCountObj(view, owner, offer, -1); + adjustOwnerCount(view, owner, {}, -1, beast::Journal{beast::Journal::getNullSink()}); view.erase(offer); return true; @@ -753,7 +728,7 @@ repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner) { Throw( "NFTokenPage directory for " + to_string(owner) + - " cannot be repaired. std::unexpected link problem."); + " cannot be repaired. Unexpected link problem."); } newPrev->at(sfNextPageMin) = nextPage->key(); view.update(newPrev); @@ -927,7 +902,6 @@ tokenOfferCreatePreclaim( TER tokenOfferCreateApply( ApplyView& view, - STTx const& tx, AccountID const& acctID, STAmount const& amount, std::optional const& dest, @@ -939,14 +913,8 @@ tokenOfferCreateApply( std::uint32_t txFlags) { Keylet const acctKeylet = keylet::account(acctID); - auto const acct = view.read(acctKeylet); - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = - checkInsufficientReserve(view, tx, acct, priorBalance, *sponsorSle, 1, 0, j); - !isTesSuccess(ret)) - return ret; + if (auto const acct = view.read(acctKeylet); priorBalance < accountReserve(view, acct, j, 1)) + return tecINSUFFICIENT_RESERVE; auto const offerID = keylet::nftoffer(acctID, seqProxy.value()); @@ -993,13 +961,11 @@ tokenOfferCreateApply( if (dest) (*offer)[sfDestination] = *dest; - addSponsorToLedgerEntry(offer, *sponsorSle); - view.insert(offer); } // Update owner count. - adjustOwnerCount(view, view.peek(acctKeylet), *sponsorSle, 1, j); + adjustOwnerCount(view, acctID, {}, 1, j); return tesSUCCESS; } diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index c26146b86c..a3b194586b 100644 --- a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -5,13 +5,20 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include +#include +#include +#include + namespace xrpl { TER @@ -59,4 +66,28 @@ closeChannel(SLE::ref slep, ApplyView& view, uint256 const& key, beast::Journal return tesSUCCESS; } +uint32_t +saturatingAdd(Rules const& rules, uint32_t const lhs, uint32_t const rhs) +{ + if (rules.enabled(fixCleanup3_2_0)) + { + static constexpr auto kUint32Max = + static_cast(std::numeric_limits::max()); + uint64_t const saturatedResult = std::min(uint64_t{lhs} + rhs, kUint32Max); + return static_cast(saturatedResult); + } + + return lhs + rhs; +} + +bool +isChannelExpired(ApplyView const& view, std::optional timeField) +{ + if (!timeField) + return false; + if (view.rules().enabled(fixCleanup3_2_0)) + return after(view.header().parentCloseTime, *timeField); + return view.header().parentCloseTime.time_since_epoch().count() >= *timeField; +} + } // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index c715894145..8f5b3f0563 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -665,7 +665,9 @@ addEmptyHolding( return tecDUPLICATE; SLE::pointer sponsorSle; - if (!isPseudoAccount(sleDst)) + + // A reserve sponsor only covers tx.Account's own objects. + if (!isPseudoAccount(sleDst) && accountID == tx[sfAccount]) { auto sle = getTxReserveSponsor(view, tx); if (!sle) diff --git a/src/libxrpl/nodestore/ManagerImp.cpp b/src/libxrpl/nodestore/ManagerImp.cpp index 5f366079f1..a3db22ce74 100644 --- a/src/libxrpl/nodestore/ManagerImp.cpp +++ b/src/libxrpl/nodestore/ManagerImp.cpp @@ -45,8 +45,10 @@ ManagerImp::missingBackend() // the Factory classes is an undefined behaviour. void registerNuDBFactory(Manager& manager); +#if XRPL_ROCKSDB_AVAILABLE void registerRocksDBFactory(Manager& manager); +#endif void registerNullFactory(Manager& manager); void @@ -55,7 +57,9 @@ registerMemoryFactory(Manager& manager); ManagerImp::ManagerImp() { registerNuDBFactory(*this); +#if XRPL_ROCKSDB_AVAILABLE registerRocksDBFactory(*this); +#endif registerNullFactory(*this); registerMemoryFactory(*this); } diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index 252ff32ccf..565fbded5d 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -1,13 +1,23 @@ +#if XRPL_ROCKSDB_AVAILABLE +#include #include #include +#include +#include +#include #include #include #include #include #include +#include +#include #include #include #include +#include +#include +#include #include #include @@ -25,26 +35,14 @@ #include #include +#include #include #include #include +#include #include #include -#if XRPL_ROCKSDB_AVAILABLE -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - namespace xrpl::NodeStore { class RocksDBEnv : public rocksdb::EnvWrapper diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 7de1862dfc..820936a22d 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0-rc3" +char const* const versionString = "3.3.0-b0" // clang-format on ; diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 6974b20568..7aa240c7ca 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -320,7 +320,7 @@ signers(AccountID const& account) noexcept } Keylet -sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept +sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept { return {ltSPONSORSHIP, indexHash(LedgerNameSpace::Sponsorship, sponsor, sponsee)}; } diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index ce3baeb35e..3aa9705b03 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -1,91 +1,136 @@ #include #include +#include #include -#include // IWYU pragma: keep #include +#include +#include +#include +#include // IWYU pragma: keep #include +#include #include #include #include +#include #include +#include +#include +#include +#include namespace xrpl { +Permission::GranularPermissionEntry::GranularPermissionEntry( + std::string name, + TxType txType, + std::uint32_t permittedFlags, + std::vector permittedFields) + : name(std::move(name)) + , txType(txType) + , permittedFlags(permittedFlags) + , permittedFields(std::move(permittedFields), TxFormats::getCommonFields()) +{ +} + Permission::Permission() { - txFeatureMap_ = { -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, amendment, ...) {value, amendment}, - -#include - -#undef TRANSACTION -#pragma pop_macro("TRANSACTION") - }; - - delegableTx_ = { -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, ...) {value, delegable}, - -#include - -#undef TRANSACTION -#pragma pop_macro("TRANSACTION") - }; - - granularPermissionMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION - -#define PERMISSION(type, txType, value) {#type, type}, - -#include - -#undef PERMISSION -#pragma pop_macro("PERMISSION") - }; - - granularNameMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION - -#define PERMISSION(type, txType, value) {type, #type}, - -#include - -#undef PERMISSION -#pragma pop_macro("PERMISSION") - }; - - granularTxTypeMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION - -#define PERMISSION(type, txType, value) {type, txType}, - -#include - -#undef PERMISSION -#pragma pop_macro("PERMISSION") - }; - - XRPL_ASSERT( - txFeatureMap_.size() == delegableTx_.size(), - "xrpl::Permission : txFeatureMap_ and delegableTx_ must have same " - "size"); - - for ([[maybe_unused]] auto const& permission : granularPermissionMap_) { - XRPL_ASSERT( - permission.second > UINT16_MAX, - "xrpl::Permission::granularPermissionMap_ : granular permission " - "value must not exceed the maximum uint16_t value."); +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegable, amendment, ...) \ + txDelegationMap_[static_cast(value)] = {amendment, delegable}; + +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + } + + granularPermissionsByName_ = { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +#define GRANULAR_PERMISSION(type, ...) {#type, type}, + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + }; + + { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \ + granularPermissions_.emplace( \ + std::piecewise_construct, \ + std::forward_as_tuple(GranularPermissionType::type), \ + std::forward_as_tuple( \ + #type, txType, static_cast(flags), std::vector fields)); + // NOLINTEND(bugprone-macro-parentheses) + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + } + + if (granularPermissionsByName_.size() != granularPermissions_.size()) + { + // LCOV_EXCL_START + Throw( + "granularPermissionsByName_ and granularPermissions_ must have same size"); + // LCOV_EXCL_STOP + } + + for (auto const& [name, type] : granularPermissionsByName_) + { + if (type <= UINT16_MAX) + { + // LCOV_EXCL_START + Throw( + "Granular permission value must exceed the maximum uint16_t value: " + name); + // LCOV_EXCL_STOP + } + } + + for (auto const& [type, entry] : granularPermissions_) + granularTxTypes_.insert(entry.txType); + + // Validate that all fields listed in permissions.macro exist in the + // corresponding transaction type's format, catching typos at startup. + for (auto const& [type, entry] : granularPermissions_) + { + if (!txDelegationMap_.contains(entry.txType)) + { + // LCOV_EXCL_START + Throw("Invalid granular permission txType in txDelegationMap_"); + // LCOV_EXCL_STOP + } + + auto const* fmt = TxFormats::getInstance().findByType(entry.txType); + if (fmt == nullptr) + { + // LCOV_EXCL_START + Throw("Invalid granular permission txType"); + // LCOV_EXCL_STOP + } + + for (auto const& field : entry.permittedFields) + { + if (fmt->getSOTemplate().getIndex(field.sField()) == -1) + { + // LCOV_EXCL_START + Throw("Invalid granular permission field"); + // LCOV_EXCL_STOP + } + } } } @@ -97,8 +142,11 @@ Permission::getInstance() } std::optional -Permission::getPermissionName(std::uint32_t const value) const +Permission::getPermissionName(std::uint32_t value) const { + if (value == 0) + return std::nullopt; + auto const permissionValue = static_cast(value); if (auto const granular = getGranularName(permissionValue)) return granular; @@ -114,90 +162,131 @@ Permission::getPermissionName(std::uint32_t const value) const std::optional Permission::getGranularValue(std::string const& name) const { - auto const it = granularPermissionMap_.find(name); - if (it != granularPermissionMap_.end()) + auto const it = granularPermissionsByName_.find(name); + if (it != granularPermissionsByName_.end()) return static_cast(it->second); return std::nullopt; } std::optional -Permission::getGranularName(GranularPermissionType const& value) const +Permission::getGranularName(GranularPermissionType value) const { - auto const it = granularNameMap_.find(value); - if (it != granularNameMap_.end()) - return it->second; + auto const it = granularPermissions_.find(value); + if (it != granularPermissions_.end()) + return it->second.name; return std::nullopt; } std::optional -Permission::getGranularTxType(GranularPermissionType const& gpType) const +Permission::getGranularTxType(GranularPermissionType gpType) const { - auto const it = granularTxTypeMap_.find(gpType); - if (it != granularTxTypeMap_.end()) - return it->second; + auto const it = granularPermissions_.find(gpType); + if (it != granularPermissions_.end()) + return it->second.txType; return std::nullopt; } +bool +Permission::hasGranularPermissions(TxType txType) const +{ + return granularTxTypes_.contains(txType); +} + std::optional> Permission::getTxFeature(TxType txType) const { - auto const txFeaturesIt = txFeatureMap_.find(txType); + auto const it = txDelegationMap_.find(txType); XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "xrpl::Permissions::getTxFeature : tx exists in txFeatureMap_"); + it != txDelegationMap_.end(), + "xrpl::Permission::getTxFeature : tx exists in txDelegationMap_"); - if (txFeaturesIt->second == uint256{}) + if (it->second.amendment == uint256{}) return std::nullopt; - return txFeaturesIt->second; + + return std::optional{std::cref(it->second.amendment)}; } bool -Permission::isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const +Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const { - auto const granularPermission = - getGranularName(static_cast(permissionValue)); - if (granularPermission) + if (permissionValue == 0) + return false; // LCOV_EXCL_LINE + + auto const amendmentEnabled = [&rules](TxDelegationEntry const& entry) { + return entry.amendment == uint256{} || rules.enabled(entry.amendment); + }; + + // Granular permissions may authorize a limited subset of a tx type even + // when the full tx type is not delegable. They still require the + // underlying transaction amendment to be enabled. + if (auto const granularIt = + granularPermissions_.find(static_cast(permissionValue)); + granularIt != granularPermissions_.end()) { - // granular permissions are always allowed to be delegated - return true; + auto const txIt = txDelegationMap_.find(granularIt->second.txType); + return txIt != txDelegationMap_.end() && amendmentEnabled(txIt->second); } auto const txType = permissionToTxType(permissionValue); - auto const it = delegableTx_.find(txType); + auto const txIt = txDelegationMap_.find(txType); - if (it == delegableTx_.end()) - return false; - - auto const txFeaturesIt = txFeatureMap_.find(txType); - XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "xrpl::Permissions::isDelegable : tx exists in txFeatureMap_"); - - // Delegation is only allowed if the required amendment for the transaction - // is enabled. For transactions that do not require an amendment, delegation - // is always allowed. - if (txFeaturesIt->second != uint256{} && !rules.enabled(txFeaturesIt->second)) - return false; - - if (it->second == Delegation::NotDelegable) - return false; - - return true; + // Tx-level permissions require the transaction type itself to be delegable, and + // the corresponding amendment enabled. + return txIt != txDelegationMap_.end() && txIt->second.delegable != NotDelegable && + amendmentEnabled(txIt->second); } uint32_t -Permission::txToPermissionType(TxType const& type) +Permission::txToPermissionType(TxType const type) { return static_cast(type) + 1; } TxType -Permission::permissionToTxType(uint32_t const& value) +Permission::permissionToTxType(uint32_t value) { + XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0"); return static_cast(value - 1); } +bool +Permission::checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const +{ + // Build union of flags upfront to enable an early exit. Fields are not stored and + // grouped in advance to avoid heap allocation. + std::uint32_t unionFlags = 0; + for (auto const& gp : heldPermissions) + { + auto const it = granularPermissions_.find(gp); + if (it != granularPermissions_.end()) + unionFlags |= it->second.permittedFlags; + } + + // Check if flags are permitted + if ((tx.getFlags() & ~unionFlags) != 0) + return false; + + // Check if fields are permitted. Every present field must appear in at least one held + // permission's template. The common fields are included in the constructor. + for (auto const& field : tx) + { + if (field.getSType() == STI_NOTPRESENT) + continue; + + if (!std::ranges::any_of(heldPermissions, [&](auto const& gp) { + auto const it = granularPermissions_.find(gp); + return it != granularPermissions_.end() && + it->second.permittedFields.getIndex(field.getFName()) != -1; + })) + return false; + } + + return true; +} + } // namespace xrpl diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 4bcf579926..5cf768a05f 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -213,11 +213,11 @@ STTx::getSeqValue() const } AccountID -STTx::getFeePayer() const +STTx::getInitiator() const { // If sfDelegate is present, the delegate account is the payer // note: if a delegate is specified, its authorization to act on behalf of the account is - // enforced in `Transactor::checkPermission` + // enforced in `Transactor::invokeCheckPermission` // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) if (isFieldPresent(sfDelegate)) return getAccountID(sfDelegate); @@ -546,7 +546,7 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const // For delegated transactions sfDelegate is the account whose signer list is checked, // the delegate account itself can not be among the signers. auto const txnAccountID = - &sigObject != this ? std::nullopt : std::optional(getFeePayer()); + &sigObject != this ? std::nullopt : std::optional(getInitiator()); // We can ease the computational load inside the loop a bit by // pre-constructing part of the data that we hash. Fill a Serializer diff --git a/src/libxrpl/server/InfoSub.cpp b/src/libxrpl/server/InfoSub.cpp index 87b48296a1..353c295856 100644 --- a/src/libxrpl/server/InfoSub.cpp +++ b/src/libxrpl/server/InfoSub.cpp @@ -1,15 +1,47 @@ #include +#include +#include #include #include +#include #include #include +#include #include #include namespace xrpl { +namespace { + +// Wraps a Source teardown call so that an exception from one cleanup +// step does not prevent the subsequent steps from running. Source methods +// acquire a lock and can throw std::system_error; a throw out of ~InfoSub +// during stack unwinding would terminate the process. Failures are +// reported through the Source's Journal so they reach the configured log +// sinks; JLOG itself cannot throw, so the noexcept guarantee holds. +template +void +safeUnsub(std::uint64_t seq, F&& f, beast::Journal j) noexcept +{ + try + { + f(); + } + catch (std::exception const& e) + { + JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: " << e.what(); + } + catch (...) + { + JLOG(j.warn()) << "~InfoSub[seq=" << seq << "]: cleanup step failed: unknown exception"; + } +} + +} // namespace + // This is the primary interface into the "client" portion of the program. // Code that wants to do normal operations on the network such as // creating and monitoring accounts, creating transactions, and so on @@ -32,25 +64,44 @@ InfoSub::InfoSub(Source& source, Consumer consumer) InfoSub::~InfoSub() { - source_.unsubTransactions(seq_); - source_.unsubRTTransactions(seq_); - source_.unsubLedger(seq_); - source_.unsubManifests(seq_); - source_.unsubServer(seq_); - source_.unsubValidations(seq_); - source_.unsubPeerStatus(seq_); - source_.unsubConsensus(seq_); + // Each Source teardown call below acquires a server-side lock and + // can throw. Wrap each independent call so partial failure does not + // skip the remaining teardown steps. + + auto const& j = source_.journal(); + + safeUnsub(seq_, [&] { source_.unsubTransactions(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubRTTransactions(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubLedger(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubManifests(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubServer(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubValidations(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j); + safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j); // Use the internal unsubscribe so that it won't call // back to us and modify its own parameter if (!realTimeSubscriptions_.empty()) - source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); + { + safeUnsub( + seq_, [&] { source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); }, j); + } if (!normalSubscriptions_.empty()) - source_.unsubAccountInternal(seq_, normalSubscriptions_, false); + { + safeUnsub( + seq_, [&] { source_.unsubAccountInternal(seq_, normalSubscriptions_, false); }, j); + } for (auto const& account : accountHistorySubscriptions_) - source_.unsubAccountHistoryInternal(seq_, account, false); + { + safeUnsub(seq_, [&] { source_.unsubAccountHistoryInternal(seq_, account, false); }, j); + } + + for (auto const& book : bookSubscriptions_) + { + safeUnsub(seq_, [&] { source_.unsubBookInternal(seq_, book); }, j); + } } Resource::Consumer& @@ -114,6 +165,20 @@ InfoSub::deleteSubAccountHistory(AccountID const& account) accountHistorySubscriptions_.erase(account); } +void +InfoSub::insertBookSubscription(Book const& book) +{ + std::scoped_lock const sl(lock_); + bookSubscriptions_.insert(book); +} + +void +InfoSub::deleteBookSubscription(Book const& book) +{ + std::scoped_lock const sl(lock_); + bookSubscriptions_.erase(book); +} + void InfoSub::clearRequest() { diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index dd643537ec..8ce60f214b 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -46,8 +48,11 @@ #include #include #include +#include #include #include +#include +#include #include #include @@ -176,13 +181,6 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id) if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor)) return temDISABLED; - if (hasSponsorFlags && - ((ctx.tx.getFieldU32(sfSponsorFlags) & ~(spfSponsorFee | spfSponsorReserve)) != 0u)) - { - JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; - return temINVALID_FLAG; - } - if (!hasSponsor) { if (hasSponsorFlags) @@ -200,11 +198,51 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id) else if (hasSponsorFlags) { auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); - if (((sponsorFlags & ~(spfSponsorFee | spfSponsorReserve)) != 0u) || sponsorFlags == 0) + if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0) { JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags"; return temINVALID_FLAG; } + + // Reserve sponsorship is only permitted for an explicit allow-list of + // transaction types, for v1. All other tx types reject spfSponsorReserve here. + if ((sponsorFlags & spfSponsorReserve) != 0u) + { + static std::unordered_set const kReserveSponsorAllowed = { + // Explicitly allow-listed for v1. + ttDELEGATE_SET, + ttDEPOSIT_PREAUTH, + ttPAYMENT, + ttSIGNER_LIST_SET, + ttCHECK_CANCEL, + ttCHECK_CASH, + ttCHECK_CREATE, + ttESCROW_CANCEL, + ttESCROW_CREATE, + ttESCROW_FINISH, + ttPAYCHAN_CLAIM, + ttPAYCHAN_CREATE, + ttPAYCHAN_FUND, + ttCLAWBACK, + ttMPTOKEN_AUTHORIZE, + ttMPTOKEN_ISSUANCE_CREATE, + ttMPTOKEN_ISSUANCE_DESTROY, + ttMPTOKEN_ISSUANCE_SET, + ttTRUST_SET, + ttCREDENTIAL_ACCEPT, + ttCREDENTIAL_CREATE, + ttCREDENTIAL_DELETE, + ttACCOUNT_SET, + ttREGULAR_KEY_SET, + ttSPONSORSHIP_TRANSFER, + }; + if (!kReserveSponsorAllowed.contains(ctx.tx.getTxnType())) + { + JLOG(ctx.j.debug()) + << "preflight1: spfSponsorReserve not allowed for this transaction type"; + return temINVALID_FLAG; + } + } } else { @@ -233,6 +271,16 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx[sfDelegate] == ctx.tx[sfAccount]) return temBAD_SIGNER; + + auto const& perm = Permission::getInstance(); + auto const txType = ctx.tx.getTxnType(); + + // If the transaction is not delegable and does not have granular permissions, fail earlier + // with temINVALID. This is to prevent transactions that are not delegable at all from + // being processed further in the invokeCheckPermission function. + if (!perm.isDelegable(Permission::txToPermissionType(txType), ctx.rules) && + !perm.hasGranularPermissions(txType)) + return temINVALID; } if (auto const ret = preflight0(ctx, flagMask)) @@ -356,19 +404,33 @@ Transactor::preflightSigValidated(PreflightContext const& ctx) } NotTEC -Transactor::checkPermission(ReadView const& view, STTx const& tx) +Transactor::checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions) { auto const delegate = tx[~sfDelegate]; if (!delegate) return tesSUCCESS; - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - + auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate)); if (!sle) return terNO_DELEGATE_PERMISSION; - return checkTxPermission(sle, tx); + if (isTesSuccess(checkTxPermission(sle, tx))) + return tesSUCCESS; + + if (!Permission::getInstance().hasGranularPermissions(tx.getTxnType())) + return terNO_DELEGATE_PERMISSION; + + heldGranularPermissions = getGranularPermission(sle, tx.getTxnType()); + if (heldGranularPermissions.empty()) + return terNO_DELEGATE_PERMISSION; + + if (!Permission::getInstance().checkGranularSandbox(tx, heldGranularPermissions)) + return terNO_DELEGATE_PERMISSION; + + return tesSUCCESS; } NotTEC @@ -386,20 +448,16 @@ Transactor::checkSponsor(ReadView const& view, STTx const& tx) return tesSUCCESS; auto const sponsorshipSle = - view.read(keylet::sponsor(tx.getAccountID(sfSponsor), tx.getAccountID(sfAccount))); + view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getAccountID(sfAccount))); // sponsorship object missing for pre-funded tx if (!sponsorshipSle) return terNO_SPONSORSHIP; - auto const sponsorFlags = tx.getFieldU32(sfSponsorFlags); - - if (((sponsorFlags & spfSponsorFee) != 0u) && - sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee)) + if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee)) return terNO_SPONSORSHIP; - if (((sponsorFlags & spfSponsorReserve) != 0u) && - sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve)) + if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve)) return terNO_SPONSORSHIP; return tesSUCCESS; @@ -582,7 +640,28 @@ Transactor::payFee() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const feeAmountAfter = sle->getFieldAmount(feePayer.balanceField) - feePaid; + if (feePaid == beast::kZero) + return tesSUCCESS; + + XRPAmount balance = beast::kZero; + if (sle->isFieldPresent(feePayer.balanceField)) + { + balance = sle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return tefINTERNAL; // LCOV_EXCL_LINE + } + + if (feePaid > balance) + { + if ((balance > beast::kZero) && !view().open()) + return tecINSUFF_FEE; + + return terINSUF_FEE_B; + } + + auto const feeAmountAfter = balance - feePaid; if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount) { @@ -867,10 +946,9 @@ Transactor::checkSign( if (!sigObject.isFieldPresent(sfSponsor)) return tefINTERNAL; // LCOV_EXCL_LINE - auto const sponsorAccountID = sigObject.getAccountID(sfSponsor); + auto const sponsorID = sigObject.getAccountID(sfSponsor); auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature); - if (auto const ret = - checkSign(view, flags, std::nullopt, sponsorAccountID, sponsorSignature, j); + if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j); !isTesSuccess(ret)) return ret; } @@ -1222,26 +1300,6 @@ removeDeletedTrustLines( } } -static void -removeDeletedMPTs(ApplyView& view, std::vector const& mpts, beast::Journal viewJ) -{ - // There could be at most two MPTs - one for each side of AMM pool - if (mpts.size() > 2) - { - JLOG(viewJ.error()) << "removeDeletedMPTs: deleted mpts exceed 2 " << mpts.size(); - return; - } - - for (auto const& index : mpts) - { - if (auto const sleState = view.peek({ltMPTOKEN, index}); sleState && - deleteAMMMPToken(view, sleState, (*sleState)[sfIssuer], viewJ) != tesSUCCESS) - { - JLOG(viewJ.error()) << "removeDeletedMPTs: failed to delete AMM MPT"; - } - } -} - /** Reset the context, discarding any changes made and adjust the fee. @param fee The transaction fee to be charged. @@ -1265,7 +1323,15 @@ Transactor::reset(XRPAmount fee) if (!payerSle) return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE - auto const balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + XRPAmount balance = beast::kZero; + if (payerSle->isFieldPresent(feePayer.balanceField)) + { + balance = payerSle->getFieldAmount(feePayer.balanceField).xrp(); + } + else if (feePayer.balanceField != sfFeeAmount) + { + return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE + } if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee)) { @@ -1317,19 +1383,19 @@ Transactor::reset(XRPAmount fee) FeePayer Transactor::getFeePayer(ReadView const& view, STTx const& tx) { - if (tx.isFieldPresent(sfSponsor) && ((tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u)) + if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx)) { - auto const sponsorAccountID = tx.getAccountID(sfSponsor); - auto const sponseeAccountID = tx.getAccountID(sfAccount); + auto const sponsorID = tx.getAccountID(sfSponsor); + auto const sponseeID = tx.getAccountID(sfAccount); auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature); - auto const sponsorshipKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); + auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID); // if pre-funded sponsorship exists, prefer it if (hasSponsorSignature && !view.exists(sponsorshipKeylet)) { // co-signed return FeePayer{ - .entry = keylet::account(sponsorAccountID), + .entry = keylet::account(sponsorID), .balanceField = sfBalance, .type = FeePayerType::SponsorCoSigned}; } @@ -1341,7 +1407,7 @@ Transactor::getFeePayer(ReadView const& view, STTx const& tx) .type = FeePayerType::SponsorPreFunded}; } - auto const payerAccountKeylet = keylet::account(tx.getFeePayer()); + auto const payerAccountKeylet = keylet::account(tx.getInitiator()); auto const payerType = tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account; @@ -1356,6 +1422,118 @@ Transactor::trapTransaction(uint256 txHash) const JLOG(j_.debug()) << "Transaction trapped: " << txHash; } +std::tuple +Transactor::processPersistentChanges(TER result, XRPAmount fee) +{ + JLOG(j_.trace()) << "reapplying because of " << transToken(result); + + // FIXME: This mechanism for doing work while returning a `tec` is + // awkward and very limiting. A more general purpose approach + // should be used, making it possible to do more useful work + // when transactions fail with a `tec` code. + + auto typesForResult = [](TER const ter) { + std::unordered_set types; + if ((ter == tecOVERSIZE) || (ter == tecKILLED)) + { + types.insert(ltOFFER); + } + else if (ter == tecINCOMPLETE) + { + types.insert(ltRIPPLE_STATE); + } + else if (ter == tecEXPIRED) + { + types.insert(ltNFTOKEN_OFFER); + types.insert(ltCREDENTIAL); + } + return types; + }; + + // Build a list of ledger entry types to collect, based on the + // result code. Only deleted objects of these types will be + // re-applied after the context is reset. + auto const typesToCollect = typesForResult(result); + + std::map> deletedObjects; + if (!typesToCollect.empty()) + { + ctx_.visit( + [&typesToCollect, &deletedObjects]( + uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) { + if (isDelete) + { + XRPL_ASSERT( + before && after, + "xrpl::Transactor::processPersistentChanges : non-null " + "SLE inputs"); + if (before && after) + { + auto const type = before->getType(); + if (typesToCollect.contains(type)) + { + // For offers, only collect unfunded removals + // (where TakerPays is unchanged) + if (type == ltOFFER && + before->getFieldAmount(sfTakerPays) != + after->getFieldAmount(sfTakerPays)) + return; + + deletedObjects[type].push_back(index); + } + } + } + }); + } + + // Reset the context, potentially adjusting the fee. + { + auto const resetResult = reset(fee); + if (!isTesSuccess(resetResult.first)) + result = resetResult.first; + + fee = resetResult.second; + } + + // Re-apply the collected deletions, but only if the reset succeeded + // and the post-reset result still allows the same deletion type. + auto const typesToApply = typesForResult(result); + if (isTecClaim(result) && !typesToApply.empty()) + { + auto const viewJ = ctx_.registry.get().getJournal("View"); + for (auto const& [type, ids] : deletedObjects) + { + if (ids.empty() || !typesToApply.contains(type)) + continue; + + switch (type) + { + case ltOFFER: + removeUnfundedOffers(view(), ids, viewJ); + break; + case ltNFTOKEN_OFFER: + removeExpiredNFTokenOffers(view(), ids, viewJ); + break; + case ltRIPPLE_STATE: + removeDeletedTrustLines(view(), ids, viewJ); + break; + case ltCREDENTIAL: + removeExpiredCredentials(view(), ids, viewJ); + break; + // LCOV_EXCL_START + default: + UNREACHABLE( + "xrpl::Transactor::processPersistentChanges() : " + "unexpected type"); + break; + // LCOV_EXCL_STOP + } + } + } + + return {result, fee, isTecClaim(result)}; +} + [[nodiscard]] TER Transactor::checkTransactionInvariants(TER result, XRPAmount fee) { @@ -1405,6 +1583,7 @@ Transactor::checkInvariants(TER result, XRPAmount fee) */ return ctx_.checkInvariants(result, fee); } + //------------------------------------------------------------------------------ ApplyResult Transactor::operator()() @@ -1471,108 +1650,7 @@ Transactor::operator()() (result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) || (result == tecEXPIRED) || (isTecClaimHardFail(result, view().flags()))) { - JLOG(j_.trace()) << "reapplying because of " << transToken(result); - - // FIXME: This mechanism for doing work while returning a `tec` is - // awkward and very limiting. A more general purpose approach - // should be used, making it possible to do more useful work - // when transactions fail with a `tec` code. - std::vector removedOffers; - std::vector removedTrustLines; - std::vector removedMPTs; - std::vector expiredNFTokenOffers; - std::vector expiredCredentials; - - bool const doOffers = ((result == tecOVERSIZE) || (result == tecKILLED)); - bool const doLinesOrMPTs = (result == tecINCOMPLETE); - bool const doNFTokenOffers = (result == tecEXPIRED); - bool const doCredentials = (result == tecEXPIRED); - if (doOffers || doLinesOrMPTs || doNFTokenOffers || doCredentials) - { - ctx_.visit([doOffers, - &removedOffers, - doLinesOrMPTs, - &removedTrustLines, - &removedMPTs, - doNFTokenOffers, - &expiredNFTokenOffers, - doCredentials, - &expiredCredentials]( - uint256 const& index, - bool isDelete, - SLE::const_ref before, - SLE::const_ref after) { - if (isDelete) - { - XRPL_ASSERT( - before && after, - "xrpl::Transactor::operator()::visit : non-null SLE " - "inputs"); - if (doOffers && before && after && (before->getType() == ltOFFER) && - (before->getFieldAmount(sfTakerPays) == after->getFieldAmount(sfTakerPays))) - { - // Removal of offer found or made unfunded - removedOffers.push_back(index); - } - - if (doLinesOrMPTs && before && after) - { - // Removal of obsolete AMM trust line - if (before->getType() == ltRIPPLE_STATE) - { - removedTrustLines.push_back(index); - } - else if (before->getType() == ltMPTOKEN) - { - removedMPTs.push_back(index); - } - } - - if (doNFTokenOffers && before && after && - (before->getType() == ltNFTOKEN_OFFER)) - expiredNFTokenOffers.push_back(index); - - if (doCredentials && before && after && (before->getType() == ltCREDENTIAL)) - expiredCredentials.push_back(index); - } - }); - } - - // Reset the context, potentially adjusting the fee. - { - auto const resetResult = reset(fee); - if (!isTesSuccess(resetResult.first)) - result = resetResult.first; - - fee = resetResult.second; - } - - // If necessary, remove any offers found unfunded during processing - if ((result == tecOVERSIZE) || (result == tecKILLED)) - { - removeUnfundedOffers(view(), removedOffers, ctx_.registry.get().getJournal("View")); - } - - if (result == tecEXPIRED) - { - removeExpiredNFTokenOffers( - view(), expiredNFTokenOffers, ctx_.registry.get().getJournal("View")); - } - - if (result == tecINCOMPLETE) - { - removeDeletedTrustLines( - view(), removedTrustLines, ctx_.registry.get().getJournal("View")); - removeDeletedMPTs(view(), removedMPTs, ctx_.registry.get().getJournal("View")); - } - - if (result == tecEXPIRED) - { - removeExpiredCredentials( - view(), expiredCredentials, ctx_.registry.get().getJournal("View")); - } - - applied = isTecClaim(result); + std::tie(result, fee, applied) = processPersistentChanges(result, fee); } if (applied) diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 3691fbd434..5af4f621a7 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -184,7 +184,8 @@ invokePreclaim(PreclaimContext const& ctx) if (NotTEC const result = T::checkSponsor(ctx.view, ctx.tx)) return result; - if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx)) + if (NotTEC const result = + Transactor::invokeCheckPermission(ctx.view, ctx.tx)) return result; if (NotTEC const result = T::checkSign(ctx)) diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp b/src/libxrpl/tx/invariants/AMMInvariant.cpp index ecd7bedf89..cca0ce149c 100644 --- a/src/libxrpl/tx/invariants/AMMInvariant.cpp +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp @@ -27,7 +27,14 @@ void ValidAMM::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { if (isDelete) + { + if (before && before->getType() == ltAMM) + { + ammDeleted_ = true; + lptAMMBalanceBeforeDeletion_ = before->getFieldAmount(sfLPTokenBalance); + } return; + } if (after) { @@ -166,18 +173,60 @@ ValidAMM::finalizeCreate( } bool -ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +ValidAMM::finalizeDelete(bool enforce, bool enforceAMMDelete, TER res, beast::Journal const& j) + const { if (ammAccount_) { // LCOV_EXCL_START - std::string const msg = (isTesSuccess(res)) ? "AMM object is not deleted on tesSUCCESS" - : "AMM object is changed on tecINCOMPLETE"; + std::string const msg = (isTesSuccess(res)) ? "AMM object remained on tesSUCCESS" + : "AMM object changed on tecINCOMPLETE"; JLOG(j.error()) << "Invariant failed: AMMDelete failed, " << msg; if (enforce) return false; // LCOV_EXCL_STOP } + if (enforceAMMDelete) + { + if (isTesSuccess(res)) + { + if (!ammDeleted_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS"; + return false; + // LCOV_EXCL_STOP + } + if (!lptAMMBalanceBeforeDeletion_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "Invariant failed: AMMDelete failed, AMM object deleted without LP balance"; + return false; + // LCOV_EXCL_STOP + } + if (*lptAMMBalanceBeforeDeletion_ != beast::kZero) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP " + "balance: " + << *lptAMMBalanceBeforeDeletion_; + return false; + // LCOV_EXCL_STOP + } + } + else if (ammDeleted_) + { + // AMM should only be fully deleted when AMMDelete returns tesSUCCESS. + // LCOV_EXCL_START + JLOG(j.error()) << "Invariant failed: AMMDelete failed, AMM object deleted when result " + "is not tesSUCCESS"; + return false; + // LCOV_EXCL_STOP + } + } return true; } @@ -221,13 +270,8 @@ ValidAMM::generalInvariant( auto const poolProductMean = root2(amount * amount2); bool const nonNegativeBalances = validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); - bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; - // Allow for a small relative error if strongInvariantCheck fails - auto weakInvariantCheck = [&]() { - return *lptAMMBalanceAfter_ != beast::kZero && - withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); - }; - if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck())) + auto const precisionLoss = checkAMMPrecisionLoss(poolProductMean, *lptAMMBalanceAfter_); + if (!nonNegativeBalances || !isTesSuccess(precisionLoss)) { JLOG(j.error()) << "Invariant failed: AMM " << tx.getTxnType() << " " << tx.getHash(HashPrefix::TransactionId) << " " << ammPoolChanged_ << " " @@ -271,16 +315,20 @@ ValidAMM::finalizeWithdraw( xrpl::STTx const& tx, xrpl::ReadView const& view, bool enforce, + bool enforceAMMDelete, beast::Journal const& j) const { - if (!ammAccount_) + if (enforceAMMDelete && ammDeleted_) { - // Last Withdraw or Clawback deleted AMM + // Last Withdraw or Clawback can delete the AMM. We don't have to check + // the LPToken balance because a final AMMWithdraw or AMMClawback can + // redeem the remaining LP tokens and delete the AMM entry in the same + // transaction. + return true; } - else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + if (ammAccount_ && !generalInvariant(tx, view, ZeroAllowed::Yes, j) && enforce) { - if (enforce) - return false; + return false; } return true; @@ -300,6 +348,25 @@ ValidAMM::finalize( return true; bool const enforce = view.rules().enabled(fixAMMv1_3); + bool const enforceAMMDelete = view.rules().enabled(fixCleanup3_3_0); + + // AMM can only be deleted by AMMWithdraw, AMMClawback, and AMMDelete + if (enforceAMMDelete && ammDeleted_) + { + switch (tx.getTxnType()) + { + case ttAMM_WITHDRAW: + case ttAMM_CLAWBACK: + case ttAMM_DELETE: + break; + default: + // LCOV_EXCL_START + JLOG(j.error()) << "Invariant failed: AMM failed, unexpected AMM deletion by " + << tx.getTxnType(); + return false; + // LCOV_EXCL_STOP + } + } switch (tx.getTxnType()) { @@ -309,13 +376,13 @@ ValidAMM::finalize( return finalizeDeposit(tx, view, enforce, j); case ttAMM_CLAWBACK: case ttAMM_WITHDRAW: - return finalizeWithdraw(tx, view, enforce, j); + return finalizeWithdraw(tx, view, enforce, enforceAMMDelete, j); case ttAMM_BID: return finalizeBid(enforce, j); case ttAMM_VOTE: return finalizeVote(enforce, j); case ttAMM_DELETE: - return finalizeDelete(enforce, result, j); + return finalizeDelete(enforce, enforceAMMDelete, result, j); case ttCHECK_CASH: case ttOFFER_CREATE: case ttPAYMENT: diff --git a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp index 41d55e9019..9ebc008473 100644 --- a/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp +++ b/src/libxrpl/tx/invariants/SponsorshipInvariant.cpp @@ -4,13 +4,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include @@ -62,7 +62,7 @@ SponsorshipOwnerCountsMatch::visitEntry( if (!sle->isFieldPresent(sfSponsor)) return 0; auto const priceDataSeries = sle->getFieldArray(sfPriceDataSeries); - return OracleSet::calculateOracleReserve(priceDataSeries.size()); + return calculateOracleReserve(priceDataSeries.size()); } case ltVAULT: { if (!sle->isFieldPresent(sfSponsor)) @@ -93,7 +93,7 @@ SponsorshipOwnerCountsMatch::visitEntry( (afterSponsoredObjectOwnerCount - beforeSponsoredObjectOwnerCount); if (getOwnerCount(after) < getSponsored(after)) - invalidOwnerCountLessThanSponsoredOwnerCount_ += 1; + ownerCountBelowSponsored_ += 1; } bool @@ -111,6 +111,13 @@ SponsorshipOwnerCountsMatch::finalize( return false; } + if (ownerCountBelowSponsored_ > 0) + { + JLOG(j.fatal()) + << "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount."; + return false; + } + if (deltaSponsoredObjectOwnerCount_ != deltaSponsoredOwnerCount_) { JLOG(j.fatal()) << "Invariant failed: SponsoredObjectOwnerCount does not " @@ -118,13 +125,6 @@ SponsorshipOwnerCountsMatch::finalize( return false; } - if (invalidOwnerCountLessThanSponsoredOwnerCount_ > 0) - { - JLOG(j.fatal()) - << "Invariant failed: OwnerCount must be greater than or equal to SponsoredOwnerCount."; - return false; - } - return true; } diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 80b8f36bd9..ad29617f6b 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -1048,6 +1048,10 @@ ValidVault::finalize( // TBD return true; } + case ttSPONSORSHIP_TRANSFER: { + // SponsorshipTransfer may update a vault's sfSponsor + return true; + } default: // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp index 07dd8628f3..f434b33548 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipSet.cpp @@ -1,25 +1,24 @@ #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 { @@ -47,10 +46,10 @@ SponsorshipSet::preflight(PreflightContext const& ctx) if (hasSponsor == hasSponsee) return temMALFORMED; - auto const sponsorAccountID = ctx.tx[~sfCounterpartySponsor].value_or(account); - auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(account); + auto const sponsorID = ctx.tx[~sfCounterpartySponsor].value_or(account); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); - if (sponsorAccountID == sponseeAccountID) + if (sponsorID == sponseeID) return temMALFORMED; if (ctx.tx.isFlag(tfDeleteObject)) @@ -64,7 +63,7 @@ SponsorshipSet::preflight(PreflightContext const& ctx) return temINVALID_FLAG; // can not include these fields when deleting - if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfReserveCount) || + if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfRemainingOwnerCount) || ctx.tx.isFieldPresent(sfMaxFee)) return temMALFORMED; } @@ -72,7 +71,7 @@ SponsorshipSet::preflight(PreflightContext const& ctx) { // although both Sponsor and Sponsee can delete, // only the Sponsor can create or update sponsorship. - if (account != sponsorAccountID) + if (account != sponsorID) return temMALFORMED; // Check FeeAmount and MaxFee @@ -101,64 +100,22 @@ SponsorshipSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -SponsorshipSet::checkPermission(ReadView const& view, STTx const& tx) -{ - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (checkTxPermission(sle, tx) == tesSUCCESS) - return tesSUCCESS; - - auto const txFlags = tx.getFlags(); - - // this is added in case more flags will be added for SponsorshipSet - // in the future. Currently unreachable. - if ((txFlags & tfSponsorshipSetPermissionMask) != 0u) - return terNO_DELEGATE_PERMISSION; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttSPONSORSHIP_SET, granularPermissions); - - auto const sponsoringFee = tx.isFieldPresent(sfFeeAmount) || tx.isFieldPresent(sfMaxFee) || - ((txFlags & (tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee)) != - 0u); - auto const sponsoringReserve = tx.isFieldPresent(sfReserveCount) || - ((txFlags & - (tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForReserve)) != 0u); - - if (sponsoringFee && !granularPermissions.contains(SponsorFee)) - return terNO_DELEGATE_PERMISSION; - - if (sponsoringReserve && !granularPermissions.contains(SponsorReserve)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER SponsorshipSet::preclaim(PreclaimContext const& ctx) { - auto const sponsorAccountID = ctx.tx[~sfCounterpartySponsor].value_or(ctx.tx[sfAccount]); - auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(ctx.tx[sfAccount]); + auto const sponsorID = ctx.tx[~sfCounterpartySponsor].value_or(ctx.tx[sfAccount]); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(ctx.tx[sfAccount]); - if (sponseeAccountID == sponsorAccountID) + if (sponseeID == sponsorID) return tecINTERNAL; // LCOV_EXCL_LINE // check Sponsor - auto const sponsorAccSle = ctx.view.read(keylet::account(sponsorAccountID)); + auto const sponsorAccSle = ctx.view.read(keylet::account(sponsorID)); if (!sponsorAccSle) return tecNO_DST; // check Sponsee - auto const sponseeSle = ctx.view.read(keylet::account(sponseeAccountID)); + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); if (!sponseeSle) return tecNO_DST; @@ -167,79 +124,97 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; // check if object exists - auto const sponsorObjSle = ctx.view.read(keylet::sponsor(sponsorAccountID, sponseeAccountID)); + auto const sponsorshipSle = ctx.view.read(keylet::sponsorship(sponsorID, sponseeID)); - if (ctx.tx.isFlag(tfDeleteObject) && !sponsorObjSle) + if (ctx.tx.isFlag(tfDeleteObject) && !sponsorshipSle) return tecNO_ENTRY; return tesSUCCESS; } +static TER +deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsorID = (*sle)[sfOwner]; + auto const sponseeID = (*sle)[sfSponsee]; + + // The reserve for the Sponsorship object is held by the sponsor (Owner). + auto sponsorAccSle = view.peek(keylet::account(sponsorID)); + if (!sponsorAccSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove(keylet::ownerDir(sponsorID), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsor."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + if (!view.dirRemove(keylet::ownerDir(sponseeID), (*sle)[sfSponseeNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Sponsorship from sponsee."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + adjustOwnerCountObj(view, sponsorAccSle, sle, -1, j); + + // transfer feeAmount back to the sponsor + if (sle->isFieldPresent(sfFeeAmount)) + (*sponsorAccSle)[sfBalance] += sle->getFieldAmount(sfFeeAmount); + + view.erase(sle); + + return tesSUCCESS; +} + TER SponsorshipSet::doApply() { - auto const sponsorAccountID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); - auto const sponseeAccountID = ctx_.tx[~sfSponsee].value_or(accountID_); + auto const sponsorID = ctx_.tx[~sfCounterpartySponsor].value_or(accountID_); + auto const sponseeID = ctx_.tx[~sfSponsee].value_or(accountID_); - if (sponseeAccountID == sponsorAccountID) + if (sponseeID == sponsorID) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorAccountID)); + auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorID)); if (!sponsorAccSle) return tecINTERNAL; // LCOV_EXCL_LINE - if (!ctx_.view().exists(keylet::account(sponseeAccountID))) + if (!ctx_.view().exists(keylet::account(sponseeID))) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sponsorKeylet = keylet::sponsor(sponsorAccountID, sponseeAccountID); - auto const sponsorObjSle = ctx_.view().peek(sponsorKeylet); + auto const sponsorKeylet = keylet::sponsorship(sponsorID, sponseeID); + auto const sponsorshipSle = ctx_.view().peek(sponsorKeylet); if (ctx_.tx.isFlag(tfDeleteObject)) { // Delete - if (!sponsorObjSle) + if (!sponsorshipSle) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCountObj(ctx_.view(), sponsorAccSle, sponsorObjSle, -1, ctx_.journal); - - ctx_.view().dirRemove( - keylet::ownerDir(sponsorAccountID), - (*sponsorObjSle)[sfOwnerNode], - sponsorObjSle->key(), - false); - ctx_.view().dirRemove( - keylet::ownerDir(sponseeAccountID), - (*sponsorObjSle)[sfSponseeNode], - sponsorObjSle->key(), - false); - - // transfer feeAmount from ledger entry - if (sponsorObjSle->isFieldPresent(sfFeeAmount)) - { - auto const feeAmount = sponsorObjSle->getFieldAmount(sfFeeAmount); - (*sponsorAccSle)[sfBalance] += feeAmount; - } - - ctx_.view().erase(sponsorObjSle); - - return tesSUCCESS; + return deleteSponsorship(ctx_.view(), sponsorshipSle, ctx_.journal); } auto const feeAmount = ctx_.tx[~sfFeeAmount]; auto const maxFee = ctx_.tx[~sfMaxFee]; - auto const reserveCount = ctx_.tx[~sfReserveCount]; + auto const remainingOwnerCount = ctx_.tx[~sfRemainingOwnerCount]; auto reserveSponsorAccSle = getTxReserveSponsor(view(), ctx_.tx); if (!reserveSponsorAccSle) return reserveSponsorAccSle.error(); // LCOV_EXCL_LINE - if (!sponsorObjSle) + if (!sponsorshipSle) { // Create auto newSle = std::make_shared(sponsorKeylet); - (*newSle)[sfOwner] = sponsorAccountID; - (*newSle)[sfSponsee] = sponseeAccountID; + (*newSle)[sfOwner] = sponsorID; + (*newSle)[sfSponsee] = sponseeID; if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance]) return tecUNFUNDED; @@ -263,8 +238,8 @@ SponsorshipSet::doApply() if (maxFee && *maxFee > XRPAmount(0)) (*newSle)[sfMaxFee] = *maxFee; - if (reserveCount && *reserveCount > 0) - (*newSle)[sfReserveCount] = *reserveCount; + if (remainingOwnerCount && *remainingOwnerCount > 0) + (*newSle)[sfRemainingOwnerCount] = *remainingOwnerCount; auto flags = 0; if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) @@ -276,13 +251,13 @@ SponsorshipSet::doApply() (*newSle)[sfFlags] = flags; auto const sponsorPage = view().dirInsert( - keylet::ownerDir(sponsorAccountID), sponsorKeylet, describeOwnerDir(sponsorAccountID)); + keylet::ownerDir(sponsorID), sponsorKeylet, describeOwnerDir(sponsorID)); if (!sponsorPage) return tecDIR_FULL; // LCOV_EXCL_LINE (*newSle)[sfOwnerNode] = *sponsorPage; auto const sponseePage = view().dirInsert( - keylet::ownerDir(sponseeAccountID), sponsorKeylet, describeOwnerDir(sponseeAccountID)); + keylet::ownerDir(sponseeID), sponsorKeylet, describeOwnerDir(sponseeID)); if (!sponseePage) return tecDIR_FULL; // LCOV_EXCL_LINE (*newSle)[sfSponseeNode] = *sponseePage; @@ -298,7 +273,7 @@ SponsorshipSet::doApply() // Update if (feeAmount) { - auto const currentFeeAmount = (*sponsorObjSle)[~sfFeeAmount].valueOr(XRPAmount(0)); + auto const currentFeeAmount = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount(0)); auto feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) @@ -311,11 +286,11 @@ SponsorshipSet::doApply() if (*feeAmount == XRPAmount(0)) { - (*sponsorObjSle).makeFieldAbsent(sfFeeAmount); + (*sponsorshipSle).makeFieldAbsent(sfFeeAmount); } else { - (*sponsorObjSle).setFieldAmount(sfFeeAmount, *feeAmount); + (*sponsorshipSle).setFieldAmount(sfFeeAmount, *feeAmount); } if (auto const ret = checkInsufficientReserve( @@ -336,19 +311,19 @@ SponsorshipSet::doApply() { if (*maxFee == XRPAmount(0)) { - (*sponsorObjSle).makeFieldAbsent(sfMaxFee); + (*sponsorshipSle).makeFieldAbsent(sfMaxFee); } else { - (*sponsorObjSle)[sfMaxFee] = *maxFee; + (*sponsorshipSle)[sfMaxFee] = *maxFee; } } - if (reserveCount) - sponsorObjSle->at(sfReserveCount) = *reserveCount; + if (remainingOwnerCount) + sponsorshipSle->at(sfRemainingOwnerCount) = *remainingOwnerCount; // update Flags - auto flags = sponsorObjSle->getFieldU32(sfFlags); + auto flags = sponsorshipSle->getFieldU32(sfFlags); if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) flags |= lsfSponsorshipRequireSignForFee; @@ -361,10 +336,10 @@ SponsorshipSet::doApply() if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) flags &= ~lsfSponsorshipRequireSignForReserve; - if (flags != (*sponsorObjSle)[sfFlags]) - (*sponsorObjSle)[sfFlags] = flags; + if (flags != (*sponsorshipSle)[sfFlags]) + (*sponsorshipSle)[sfFlags] = flags; - view().update(sponsorObjSle); + view().update(sponsorshipSle); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp index 79995c07a9..deea6edc84 100644 --- a/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp @@ -16,15 +16,24 @@ #include #include #include -#include #include #include +#include #include #include namespace xrpl { +static std::optional +applyCountDelta(std::uint32_t current, std::int64_t delta) +{ + std::int64_t const next = static_cast(current) + delta; + if (next < 0 || next > std::numeric_limits::max()) + return std::nullopt; + return static_cast(next); +} + std::uint32_t SponsorshipTransfer::getFlagsMask(PreflightContext const& ctx) { @@ -44,6 +53,9 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) if (ctx.tx.isFlag(tfSponsorshipCreate)) { + // Sponsor must be included + // SponsorFlags.spfSponsorReserve must be included + // Sponsee must be excluded if (!isReserveSponsored(ctx.tx)) { JLOG(ctx.j.debug()) @@ -59,6 +71,9 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) } if (ctx.tx.isFlag(tfSponsorshipReassign)) { + // Sponsor must be included + // SponsorFlags.spfSponsorReserve must be included + // Sponsee must be excluded if (!isReserveSponsored(ctx.tx)) { JLOG(ctx.j.debug()) @@ -74,6 +89,8 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) } if (ctx.tx.isFlag(tfSponsorshipEnd)) { + // Sponsor must be excluded + // SponsorFlags.spfSponsorReserve must be excluded if (isReserveSponsored(ctx.tx)) { JLOG(ctx.j.debug()) @@ -107,141 +124,20 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) return tesSUCCESS; } -template -inline std::optional -getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account) -{ - switch (sle->getType()) - { - case ltNFTOKEN_OFFER: - case ltORACLE: - case ltPERMISSIONED_DOMAIN: - case ltVAULT: - case ltLOAN_BROKER: - return sle->getAccountID(sfOwner); - case ltCHECK: - case ltDID: - case ltTICKET: - case ltOFFER: - case ltXCHAIN_OWNED_CLAIM_ID: - case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: - case ltESCROW: - case ltPAYCHAN: - case ltMPTOKEN: - case ltDELEGATE: - case ltBRIDGE: - case ltDEPOSIT_PREAUTH: - return sle->getAccountID(sfAccount); - case ltMPTOKEN_ISSUANCE: - return sle->getAccountID(sfIssuer); - case ltLOAN: - return sle->getAccountID(sfBorrower); - case ltSIGNER_LIST: { - auto const signerList = view.read(keylet::signers(account)); - if (!signerList) - return std::nullopt; - if (signerList->key() == sle->key()) - return account; - return std::nullopt; - } - case ltCREDENTIAL: { - if (sle->isFlag(lsfAccepted)) - return sle->getAccountID(sfSubject); - return sle->getAccountID(sfIssuer); - } - case ltNFTOKEN_PAGE: { - // the upper 20 bytes of the index of ltNFTokenPage are the Owner's - // AccountID - uint256 const& key = sle->key(); - return AccountID::fromVoid(key.data()); - } - case ltRIPPLE_STATE: { - if (sle->isFlag(lsfHighReserve)) - { - auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); - if (highAccount == account) - return highAccount; - } - if (sle->isFlag(lsfLowReserve)) - { - auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); - if (lowAccount == account) - return lowAccount; - } - return std::nullopt; - } - case ltACCOUNT_ROOT: { - // AccountRoot is not supported for object sponsorship - return std::nullopt; - } - case ltNEGATIVE_UNL: - case ltDIR_NODE: - case ltAMENDMENTS: - case ltLEDGER_HASHES: - case ltFEE_SETTINGS: - case ltAMM: - return std::nullopt; - default: - return std::nullopt; - }; -} - -template -inline std::uint32_t -getLedgerEntryOwnerCount(T const& sle) -{ - switch (sle->getType()) - { - case ltORACLE: { - return OracleSet::calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); - } - default: - return 1; - } -}; - -template -inline SF_ACCOUNT const& -getLedgerEntrySponsorField(T const& sle, AccountID const& owner) -{ - switch (sle->getType()) - { - case ltRIPPLE_STATE: { - if (sle->isFlag(lsfHighReserve)) - { - auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer(); - if (highAccount == owner) - return sfHighSponsor; - } - if (sle->isFlag(lsfLowReserve)) - { - auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer(); - if (lowAccount == owner) - return sfLowSponsor; - } - // LCOV_EXCL_START - UNREACHABLE("Should not happen. Owner should be checked before calling this function."); - // LCOV_EXCL_STOP - } - default: - return sfSponsor; - } -}; - TER SponsorshipTransfer::preclaim(PreclaimContext const& ctx) { auto const index = ctx.tx[~sfObjectID]; - auto const newSponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!newSponsorSle) - return newSponsorSle.error(); // LCOV_EXCL_LINE + auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx); + if (!newSponsorSleExpected) + return newSponsorSleExpected.error(); // LCOV_EXCL_LINE + auto const newSponsorSle = *newSponsorSleExpected; - bool const isObjectSponsor = index != std::nullopt; + bool const isObjectSponsor = !!index; auto const account = ctx.tx[sfAccount]; - - auto const sponseeAccountID = ctx.tx[~sfSponsee].value_or(account); - auto const sponseeSle = ctx.view.read(keylet::account(sponseeAccountID)); + auto const sponseeID = ctx.tx[~sfSponsee].value_or(account); + auto const sponseeSle = ctx.view.read(keylet::account(sponseeID)); if (!sponseeSle) return tecINTERNAL; // LCOV_EXCL_LINE @@ -251,26 +147,49 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) if (!sle) return tecNO_ENTRY; - auto const ownerCountDelta = getLedgerEntryOwnerCount(sle); + // v1 scope: an object is only sponsorable via SponsorshipTransfer if + // its creating transaction type is itself permitted to set + // spfSponsorReserve (the allow-list in preflight1Sponsor). Otherwise + // an Oracle / Ticket / DID / etc. could be retroactively sponsored + // even though its creating tx cannot be, leaving downstream + // transactors with no path to maintain the sponsorship invariants. + switch (sle->getType()) + { + case ltDELEGATE: + case ltDEPOSIT_PREAUTH: + case ltMPTOKEN: + case ltMPTOKEN_ISSUANCE: + case ltCREDENTIAL: + case ltRIPPLE_STATE: + case ltSIGNER_LIST: + case ltCHECK: + case ltESCROW: + case ltPAYCHAN: + break; + default: + return tecNO_PERMISSION; + } - auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeAccountID); - if (!owner || owner != sponseeAccountID) + std::uint32_t const ownerCountDelta = 1; + + auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID); + if (!owner.has_value() || owner.value() != sponseeID) return tecNO_PERMISSION; - auto const& sponsorField = getLedgerEntrySponsorField(sle, *owner); + auto const& sponsorField = getLedgerEntrySponsorField(sle, owner.value()); if (ctx.tx.isFlag(tfSponsorshipCreate)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; - // check object is not sponsored yet + // check that the object is not sponsored yet if (sle->isFieldPresent(sponsorField)) return tecNO_PERMISSION; } else if (ctx.tx.isFlag(tfSponsorshipReassign)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check object is already ctx.sponsored @@ -279,7 +198,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { - if (*newSponsorSle) + if (newSponsorSle) return tecNO_PERMISSION; // check object is sponsored @@ -288,7 +207,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) // only the sponsor or sponsee can end sponsorship auto const sponsor = sle->getAccountID(sponsorField); - if (account != sponsor && account != sponseeAccountID) + if (account != sponsor && account != sponseeID) return tecNO_PERMISSION; } @@ -299,7 +218,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) ctx.tx, sponseeSle, sponseeSle->getFieldAmount(sfBalance), - *newSponsorSle, + newSponsorSle, ownerCountDelta, 0, ctx.j); @@ -310,7 +229,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) { if (ctx.tx.isFlag(tfSponsorshipCreate)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check account is not sponsored yet @@ -319,7 +238,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipReassign)) { - if (!*newSponsorSle) + if (!newSponsorSle) return tecNO_PERMISSION; // check account is already sponsored @@ -328,7 +247,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { - if (*newSponsorSle) + if (newSponsorSle) return tecNO_PERMISSION; // check account is sponsored @@ -337,7 +256,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) // only the sponsor or sponsee can end sponsorship auto const sponsor = sponseeSle->getAccountID(sfSponsor); - if (account != sponsor && account != sponseeAccountID) + if (account != sponsor && account != sponseeID) return tecNO_PERMISSION; } @@ -351,7 +270,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) ctx.tx, sponseeSle, sponseeSle->getFieldAmount(sfBalance), - *newSponsorSle, + newSponsorSle, 0, 1, ctx.j); @@ -362,33 +281,33 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -TER +static TER reduceReserveCount( ApplyView& view, AccountID const& account, AccountID const& sponsor, - int32_t delta) + int64_t delta) { if (delta == 0) return tesSUCCESS; if (delta > 0) return tefINTERNAL; // LCOV_EXCL_LINE - auto const sponsorKeylet = keylet::sponsor(sponsor, account); + auto const sponsorKeylet = keylet::sponsorship(sponsor, account); auto const sponsorSle = view.peek(sponsorKeylet); if (!sponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const reserveCount = sponsorSle->getFieldU32(sfReserveCount); - int32_t const afterReserveCount = reserveCount + delta; - - if (afterReserveCount < 0) + auto const afterReserveCount = + applyCountDelta(sponsorSle->getFieldU32(sfRemainingOwnerCount), delta); + if (!afterReserveCount) { // already checked in preclaim() + UNREACHABLE("xrpl::reduceReserveCount : invalid reserve count"); return tefINTERNAL; // LCOV_EXCL_LINE } - sponsorSle->at(sfReserveCount) = static_cast(afterReserveCount); + sponsorSle->at(sfRemainingOwnerCount) = *afterReserveCount; view.update(sponsorSle); return tesSUCCESS; } @@ -401,21 +320,22 @@ SponsorshipTransfer::doApply() auto const index = tx[~sfObjectID]; bool const isObjectSponsor = index != std::nullopt; - auto const sponseeAccountID = tx[~sfSponsee].value_or(accountID_); - auto const sponseeSle = view().peek(keylet::account(sponseeAccountID)); + auto const sponseeID = tx[~sfSponsee].value_or(accountID_); + auto const sponseeSle = view().peek(keylet::account(sponseeID)); if (!sponseeSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const setSponsorFieldU32 = [](auto const& sle, auto const& field, auto const& delta) { - int32_t const newValue = static_cast(sle->getFieldU32(field)) + delta; - - if (newValue < 0) + auto const setSponsorFieldU32 = + [] [[nodiscard]] (auto const& sle, auto const& field, auto const& delta) -> TER { + auto const newValue = applyCountDelta(sle->getFieldU32(field), delta); + if (!newValue) { UNREACHABLE("xrpl::SponsorshipTransfer::doApply : Invalid sponsor field value"); - return; + return tecINTERNAL; // LCOV_EXCL_LINE } - sle->at(field) = static_cast(newValue); + sle->at(field) = *newValue; + return tesSUCCESS; }; if (isObjectSponsor) @@ -427,99 +347,115 @@ SponsorshipTransfer::doApply() if (!objSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const ownerAccountID = getLedgerEntryOwner(view(), objSle, sponseeAccountID); - if (!ownerAccountID) + auto const ownerID = getLedgerEntryOwner(view(), objSle, sponseeID); + if (!ownerID) return tefINTERNAL; // LCOV_EXCL_LINE - auto const ownerSle = view().peek(keylet::account(*ownerAccountID)); + auto const ownerSle = view().peek(keylet::account(*ownerID)); if (!ownerSle) return tefINTERNAL; // LCOV_EXCL_LINE - auto const ownerCountDelta = getLedgerEntryOwnerCount(objSle); + std::int64_t const ownerCountDelta = 1; - auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerAccountID); + auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerID); if (ctx_.tx.isFlag(tfSponsorshipCreate)) { - auto const newSponsorAccountID = tx.getAccountID(sfSponsor); - XRPL_ASSERT(!!newSponsorAccountID, "New sponsor is required when creating sponsorship"); + auto const newSponsorID = tx.getAccountID(sfSponsor); + XRPL_ASSERT(!!newSponsorID, "New sponsor is required when creating sponsorship"); // update owner's sponsored count - setSponsorFieldU32(ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + if (auto const ter = + setSponsorFieldU32(ownerSle, sfSponsoredOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; view().update(ownerSle); // increment new sponsor's sponsoring count - auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + if (auto const ter = + setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; view().update(newSponsorSle); // set new sponsor to object - objSle->setAccountID(sponsorField, newSponsorAccountID); + objSle->setAccountID(sponsorField, newSponsorID); view().update(objSle); if (!hasSignature) { // use ReserveCount for pre-funded sponsoring - if (auto const ter = reduceReserveCount( - view(), sponseeAccountID, newSponsorAccountID, -ownerCountDelta); + if (auto const ter = + reduceReserveCount(view(), sponseeID, newSponsorID, -ownerCountDelta); !isTesSuccess(ter)) return ter; } } else if (ctx_.tx.isFlag(tfSponsorshipReassign)) { - auto const newSponsorAccountID = tx.getAccountID(sfSponsor); - XRPL_ASSERT( - !!newSponsorAccountID, "New sponsor is required when reassigning sponsorship"); + auto const newSponsorID = tx.getAccountID(sfSponsor); + XRPL_ASSERT(!!newSponsorID, "New sponsor is required when reassigning sponsorship"); - auto const oldSponsorAccountID = objSle->getAccountID(sponsorField); - XRPL_ASSERT( - !!oldSponsorAccountID, "Old sponsor is required when reassigning sponsorship"); + auto const oldSponsorID = objSle->getAccountID(sponsorField); + XRPL_ASSERT(!!oldSponsorID, "Old sponsor is required when reassigning sponsorship"); // decrement old sponsor's sponsoring count - auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + if (auto const ter = + setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; view().update(oldSponsorSle); // increment new sponsor's sponsoring count - auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + if (auto const ter = + setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta); + !isTesSuccess(ter)) + return ter; view().update(newSponsorSle); // set new sponsor to object - objSle->setAccountID(sponsorField, newSponsorAccountID); + objSle->setAccountID(sponsorField, newSponsorID); view().update(objSle); if (!hasSignature) { // use ReserveCount for pre-funded sponsoring - if (auto const ter = reduceReserveCount( - view(), sponseeAccountID, newSponsorAccountID, -ownerCountDelta); + if (auto const ter = + reduceReserveCount(view(), sponseeID, newSponsorID, -ownerCountDelta); !isTesSuccess(ter)) return ter; } } else if (ctx_.tx.isFlag(tfSponsorshipEnd)) { - auto const oldSponsorAccountID = objSle->getAccountID(sponsorField); - XRPL_ASSERT(!!oldSponsorAccountID, "Old sponsor is required when ending sponsorship"); + auto const oldSponsorID = objSle->getAccountID(sponsorField); + XRPL_ASSERT(!!oldSponsorID, "Old sponsor is required when ending sponsorship"); - auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE // decrement sponsored count - setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); + if (auto const ter = + setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; view().update(sponseeSle); // decrement old sponsoring count - setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + if (auto const ter = + setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta); + !isTesSuccess(ter)) + return ter; view().update(oldSponsorSle); // remove sponsor from object @@ -533,52 +469,60 @@ SponsorshipTransfer::doApply() { // create account sponsor // increment new sponsoring count - auto const newSponsorAccountID = tx.getAccountID(sfSponsor); - auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + auto const newSponsorID = tx.getAccountID(sfSponsor); + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; view().update(newSponsorSle); // set new sponsor to account - sponseeSle->setAccountID(sfSponsor, newSponsorAccountID); + sponseeSle->setAccountID(sfSponsor, newSponsorID); view().update(sponseeSle); } else if (ctx_.tx.isFlag(tfSponsorshipReassign)) { // reassign account sponsor // increment new sponsoring count - auto const newSponsorAccountID = tx.getAccountID(sfSponsor); - auto const newSponsorSle = view().peek(keylet::account(newSponsorAccountID)); + auto const newSponsorID = tx.getAccountID(sfSponsor); + auto const newSponsorSle = view().peek(keylet::account(newSponsorID)); if (!newSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1); + !isTesSuccess(ter)) + return ter; view().update(newSponsorSle); // decrement old sponsoring count - auto const oldSponsor = sponseeSle->getAccountID(sfSponsor); - auto const oldSponsorSle = view().peek(keylet::account(oldSponsor)); + auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + !isTesSuccess(ter)) + return ter; view().update(oldSponsorSle); // set new sponsor to account - sponseeSle->setAccountID(sfSponsor, newSponsorAccountID); + sponseeSle->setAccountID(sfSponsor, newSponsorID); view().update(sponseeSle); } else if (ctx_.tx.isFlag(tfSponsorshipEnd)) { // dissolve account sponsor - auto const oldSponsorAccountID = sponseeSle->getAccountID(sfSponsor); + auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor); sponseeSle->makeFieldAbsent(sfSponsor); view().update(sponseeSle); // decrement account sponsoring count - auto const oldSponsorSle = view().peek(keylet::account(oldSponsorAccountID)); + auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID)); if (!oldSponsorSle) return tefINTERNAL; // LCOV_EXCL_LINE - setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + if (auto const ter = setSponsorFieldU32(oldSponsorSle, sfSponsoringAccountCount, -1); + !isTesSuccess(ter)) + return ter; view().update(oldSponsorSle); } } diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 231783b2cc..29b119479b 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -407,8 +407,8 @@ AccountDelete::doApply() if (src->isFieldPresent(sfSponsor)) { - auto const sponsorAccountID = src->getAccountID(sfSponsor); - auto sponsorSle = view().peek(keylet::account(sponsorAccountID)); + auto const sponsorID = src->getAccountID(sfSponsor); + auto sponsorSle = view().peek(keylet::account(sponsorID)); if (!sponsorSle || !sponsorSle->isFieldPresent(sfSponsoringAccountCount)) return tefINTERNAL; // LCOV_EXCL_LINE diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp b/src/libxrpl/tx/transactors/account/AccountSet.cpp index bc207b39dc..36a7e7419f 100644 --- a/src/libxrpl/tx/transactors/account/AccountSet.cpp +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -20,13 +19,11 @@ #include #include #include -#include #include #include #include #include -#include namespace xrpl { @@ -168,54 +165,6 @@ AccountSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -AccountSet::checkPermission(ReadView const& view, STTx const& tx) -{ - // AccountSet is prohibited to be granted on a transaction level, - // but some granular permissions are allowed. - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions); - - auto const uSetFlag = tx.getFieldU32(sfSetFlag); - auto const uClearFlag = tx.getFieldU32(sfClearFlag); - // We don't support any flag based granular permission under - // AccountSet transaction. If any delegated account is trying to - // update the flag on behalf of another account, it is not - // authorized. - if (uSetFlag != 0 || uClearFlag != 0 || ((tx.getFlags() & tfUniversalMask) != 0u)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfEmailHash) && !granularPermissions.contains(AccountEmailHashSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfWalletLocator) || tx.isFieldPresent(sfNFTokenMinter)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfMessageKey) && !granularPermissions.contains(AccountMessageKeySet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfDomain) && !granularPermissions.contains(AccountDomainSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfTransferRate) && !granularPermissions.contains(AccountTransferRateSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfTickSize) && !granularPermissions.contains(AccountTickSizeSet)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER AccountSet::preclaim(PreclaimContext const& ctx) { diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index 8229f9fd7b..2a02dc232e 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -437,7 +436,7 @@ transferHelper( return tecINTERNAL; // LCOV_EXCL_LINE { - auto const reserve = accountReserve(psb, sleSrc, j, 0, 0); + auto const reserve = accountReserve(psb, sleSrc, j); auto const availableBalance = [&]() -> STAmount { STAmount curBal = (*sleSrc)[sfBalance]; @@ -726,10 +725,10 @@ finalizeClaimHelper( return result; } - adjustOwnerCountObj(outerSb, sleOwner, sleClaimID, -1, j); - // Remove the claim id from the ledger outerSb.erase(sleClaimID); + + adjustOwnerCount(outerSb, sleOwner, {}, -1, j); } } @@ -948,7 +947,6 @@ TER applyCreateAccountAttestations( ApplyView& view, RawView& rawView, - STTx const& tx, TIter attBegin, TIter attEnd, AccountID const& doorAccount, @@ -1030,11 +1028,10 @@ applyCreateAccountAttestations( // Check reserve auto const balance = (*sleDoor)[sfBalance]; - // Don't sponsor door account objects in transactions not sent by the door account - // itself - if (auto const ret = checkInsufficientReserve(psb, tx, sleDoor, balance, {}, 1, 0, j); - !isTesSuccess(ret)) - return std::unexpected(ret); // tecINSUFFICIENT_RESERVE + auto const reserve = accountReserve(view, sleDoor, j, 1); + + if (balance < reserve) + return std::unexpected(tecINSUFFICIENT_RESERVE); } std::vector atts; @@ -1139,8 +1136,7 @@ applyCreateAccountAttestations( if (!sleDoor) return tecINTERNAL; // LCOV_EXCL_LINE - // Don't sponsor door account objects in transactions not sent by the door account - // itself + // Reserve was already checked adjustOwnerCount(psb, sleDoor, {}, 1, j); psb.insert(createdSleClaimID); psb.update(sleDoor); @@ -1314,7 +1310,6 @@ attestationDoApply(ApplyContext& ctx) return applyCreateAccountAttestations( ctx.view(), ctx.rawView(), - ctx.tx, &*att, &*att + 1, thisDoor, @@ -1440,13 +1435,10 @@ XChainCreateBridge::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sleAcc, balance, *sponsorSle, 1, 0, ctx.j); - !isTesSuccess(ret)) - return ret; + auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; @@ -1488,11 +1480,7 @@ XChainCreateBridge::doApply() (*sleBridge)[sfOwnerNode] = *page; } - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), sleAcct, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(sleBridge, *sponsorSle); + adjustOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal); ctx_.view().insert(sleBridge); ctx_.view().update(sleAcct); @@ -1995,13 +1983,9 @@ XChainCreateClaimID::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; auto const balance = (*sleAcc)[sfBalance]; - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sleAcc, balance, *sponsorSle, 1, 0, ctx.j); - !isTesSuccess(ret)) - return ret; + auto const reserve = accountReserve(ctx.view, sleAcc, ctx.j, 1); + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; @@ -2057,11 +2041,7 @@ XChainCreateClaimID::doApply() (*sleClaimID)[sfOwnerNode] = *page; } - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), sleAcct, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(sleClaimID, *sponsorSle); + adjustOwnerCount(ctx_.view(), sleAcct, {}, 1, ctx_.journal); ctx_.view().insert(sleClaimID); ctx_.view().update(sleBridge); diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index 32563a536e..8cc9beba69 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -94,13 +93,11 @@ CredentialAccept::doApply() if (!sleSubject || !sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleSubject, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); - !isTesSuccess(ret)) - return ret; + { + STAmount const reserve{accountReserve(view(), sleSubject, j_, 1)}; + if (preFeeBalance_ < reserve) + return tecINSUFFICIENT_RESERVE; + } auto const credType(ctx_.tx[sfCredentialType]); Keylet const credentialKey = keylet::credential(accountID_, issuer, credType); @@ -119,10 +116,8 @@ CredentialAccept::doApply() sleCred->setFieldU32(sfFlags, lsfAccepted); view().update(sleCred); - adjustOwnerCountObj(view(), sleIssuer, sleCred, -1, j_); - removeSponsorFromLedgerEntry(sleCred); - adjustOwnerCount(view(), sleSubject, *sponsorSle, 1, j_); - addSponsorToLedgerEntry(sleCred, *sponsorSle); + adjustOwnerCount(view(), sleIssuer, {}, -1, j_); + adjustOwnerCount(view(), sleSubject, {}, 1, j_); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index 90cfbb5ca9..074a9fd9bf 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -7,7 +7,6 @@ #include #include // IWYU pragma: keep #include -#include #include #include #include @@ -131,13 +130,11 @@ CredentialCreate::doApply() if (!sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleIssuer, preFeeBalance_, *sponsorSle, 1, 0, ctx_.journal); - !isTesSuccess(ret)) - return ret; + { + STAmount const reserve{accountReserve(view(), sleIssuer, j_, 1)}; + if (preFeeBalance_ < reserve) + return tecINSUFFICIENT_RESERVE; + } sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, accountID_); @@ -155,8 +152,7 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - adjustOwnerCount(view(), sleIssuer, *sponsorSle, 1, j_); - addSponsorToLedgerEntry(sleCred, *sponsorSle); + adjustOwnerCount(view(), sleIssuer, {}, 1, j_); } if (subject == accountID_) diff --git a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp index dc6c98f95e..6def542c7d 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp @@ -29,14 +29,12 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx) return terNO_DELEGATE_PERMISSION; } -void -loadGranularPermission( - SLE::const_ref delegate, - TxType const& txType, - std::unordered_set& granularPermissions) +std::unordered_set +getGranularPermission(SLE::const_ref delegate, TxType const& txType) { + std::unordered_set granularPermissions; if (!delegate) - return; + return granularPermissions; auto const permissionArray = delegate->getFieldArray(sfPermissions); for (auto const& permission : permissionArray) @@ -47,6 +45,8 @@ loadGranularPermission( if (type && *type == txType) granularPermissions.insert(granularValue); } + + return granularPermissions; } } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index 27d0da57fd..0cc2be381f 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -225,7 +225,6 @@ AMMClawback::applyGuts(Sandbox& sb) std::tie(result, newLPTokenBalance, amountWithdraw, amount2Withdraw) = AMMWithdraw::equalWithdrawTokens( sb, - ctx_.tx, *ammSle, holder, ammAccount, @@ -259,6 +258,16 @@ AMMClawback::applyGuts(Sandbox& sb) if (!isTesSuccess(result)) return result; // LCOV_EXCL_LINE + if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) + { + if (auto const ter = + checkAMMPrecisionLoss(sb, ammAccount, asset, asset2, newLPTokenBalance, j_); + !isTesSuccess(ter)) + { + return ter; + } + } + auto const res = AMMWithdraw::deleteAMMAccountIfEmpty(sb, ammSle, newLPTokenBalance, asset, asset2, j_); if (!res.second) @@ -313,7 +322,6 @@ AMMClawback::equalWithdrawMatchingOneAmount( // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::equalWithdrawTokens( sb, - ctx_.tx, ammSle, holder, ammAccount, @@ -347,7 +355,6 @@ AMMClawback::equalWithdrawMatchingOneAmount( return AMMWithdraw::withdraw( sb, - ctx_.tx, ammSle, ammAccount, holder, @@ -368,7 +375,6 @@ AMMClawback::equalWithdrawMatchingOneAmount( // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::withdraw( sb, - ctx_.tx, ammSle, ammAccount, holder, diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index bde57f75df..b82ea203b4 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -148,43 +147,15 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return terNO_RIPPLE; } - if (ctx.view.rules().enabled(featureSponsor)) + // Check the reserve for LPToken trustline + STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); + // Insufficient reserve + if (xrpBalance <= beast::kZero) { - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - // Check the reserve for LPToken trustline - // Insufficient reserve - auto const accountSle = ctx.view.read(keylet::account(accountID)); - if (auto const ret = checkInsufficientReserve( - ctx.view, - ctx.tx, - accountSle, - accountSle->getFieldAmount(sfBalance), - *sponsorSle, - 1, - 0, - ctx.j); - !isTesSuccess(ret)) - { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; - } - } - else - { - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); - // Insufficient reserve - if (xrpBalance <= beast::kZero) - { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; - } + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; } - auto const ownerCountAdj = isReserveSponsored(ctx.tx) ? 0 : 1; - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, ownerCountAdj, ctx.j); auto insufficientBalance = [&](STAmount const& amount) { if (isXRP(amount)) return xrpBalance < amount; @@ -323,11 +294,7 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou sb.insert(ammSle); // Send LPT to LP. - auto const sponsorSle = getTxReserveSponsor(sb, ctx.tx); - if (!sponsorSle) - return {sponsorSle.error(), false}; // LCOV_EXCL_LINE - - auto res = accountSend(sb, accountId, account, lpTokens, ctx.journal, *sponsorSle); + auto res = accountSend(sb, accountId, account, lpTokens, ctx.journal); if (!isTesSuccess(res)) { JLOG(j.debug()) << "AMM Instance: failed to send LPT " << lpTokens; diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 9db26c9920..a6f6df5982 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -235,33 +234,11 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // Adjust the reserve if LP doesn't have LPToken trustline auto const sle = ctx.view.read(keylet::line(accountID, lpIssue.account, lpIssue.currency)); - - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - auto const accountSle = ctx.view.read(keylet::account(accountID)); - auto const reserveAdj = (*sponsorSle || sle) ? 0 : 1; - - if (xrpLiquid(ctx.view, accountID, reserveAdj, ctx.j) < deposit) - { - if (sle) - return tecUNFUNDED_AMM; - return tecINSUF_RESERVE_LINE; - } - - if (auto const ret = checkInsufficientReserve( - ctx.view, - ctx.tx, - accountSle, - accountSle->getFieldAmount(sfBalance) - deposit, - *sponsorSle, - 1, - !sle, - ctx.j); - *sponsorSle && !isTesSuccess(ret)) - return tecINSUF_RESERVE_LINE; - - return tesSUCCESS; + if (xrpLiquid(ctx.view, accountID, !sle, ctx.j) >= deposit) + return TER(tesSUCCESS); + if (sle) + return tecUNFUNDED_AMM; + return tecINSUF_RESERVE_LINE; } return accountFunds( ctx.view, @@ -382,37 +359,12 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) // We checked above but need to check again if depositing IOU only. if (ammLPHolds(ctx.view, *ammSle, accountID, ctx.j) == beast::kZero) { - if (ctx.view.rules().enabled(featureSponsor)) + STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); + // Insufficient reserve + if (xrpBalance <= beast::kZero) { - auto const accountSle = ctx.view.read(keylet::account(accountID)); - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - // Insufficient reserve - if (auto const ret = checkInsufficientReserve( - ctx.view, - ctx.tx, - accountSle, - accountSle->getFieldAmount(sfBalance), - *sponsorSle, - 1, - 0, - ctx.j); - !isTesSuccess(ret)) - { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; - } - } - else - { - STAmount const xrpBalance = xrpLiquid(ctx.view, accountID, 1, ctx.j); - // Insufficient reserve - if (xrpBalance <= beast::kZero) - { - JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; - return tecINSUF_RESERVE_LINE; - } + JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves"; + return tecINSUF_RESERVE_LINE; } } @@ -518,6 +470,19 @@ AMMDeposit::applyGuts(Sandbox& sb) XRPL_ASSERT( newLPTokenBalance > beast::kZero, "xrpl::AMMDeposit::applyGuts : valid new LP token balance"); + // Defensive check: deposit formulas with fixAMMv1_3 round LP tokens + // down and asset amounts up, so sqrt(pool1*pool2) >= newLPTokenBalance + // is guaranteed to hold. A precision loss failure is not expected. + if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) + { + if (auto const ter = checkAMMPrecisionLoss( + sb, ammAccountID, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], newLPTokenBalance, j_); + !isTesSuccess(ter)) + { + UNREACHABLE("xrpl::AMMDeposit::applyGuts : AMM precision loss"); + return {ter, false}; // LCOV_EXCL_LINE + } + } ammSle->setFieldAmount(sfLPTokenBalance, newLPTokenBalance); // LP depositing into AMM empty state gets the auction slot // and the voting @@ -558,10 +523,6 @@ AMMDeposit::deposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { - auto const sponsorSle = getTxReserveSponsor(view, ctx_.tx); - if (!sponsorSle) - return {sponsorSle.error(), STAmount{}}; // LCOV_EXCL_LINE - // Check account has sufficient funds. // Return true if it does, false otherwise. auto checkBalance = [&](auto const& depositAmount) -> TER { @@ -571,10 +532,8 @@ AMMDeposit::deposit( { auto const& lpIssue = lpTokensDeposit.get(); // Adjust the reserve if LP doesn't have LPToken trustline - auto const trustlineExists = - view.exists(keylet::line(accountID_, lpIssue.account, lpIssue.currency)); - auto const reserveAdj = (*sponsorSle || trustlineExists) ? 0 : 1; - if (xrpLiquid(view, accountID_, reserveAdj, j_) >= depositAmount) + auto const sle = view.read(keylet::line(accountID_, lpIssue.account, lpIssue.currency)); + if (xrpLiquid(view, accountID_, !sle, j_) >= depositAmount) return tesSUCCESS; } else if ( @@ -669,8 +628,7 @@ AMMDeposit::deposit( } // Deposit LP tokens - res = - accountSend(view, ammAccount, accountID_, lpTokensDepositActual, ctx_.journal, *sponsorSle); + res = accountSend(view, ammAccount, accountID_, lpTokensDepositActual, ctx_.journal); if (!isTesSuccess(res)) { JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit LPTokens"; diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index cfbce8fc2b..0fdfae1a9a 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -354,7 +353,6 @@ AMMWithdraw::applyGuts(Sandbox& sb) { return equalWithdrawLimit( sb, - ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -368,7 +366,6 @@ AMMWithdraw::applyGuts(Sandbox& sb) { return singleWithdrawTokens( sb, - ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -380,26 +377,17 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (subTxType & tfLimitLPToken) { return singleWithdrawEPrice( - sb, - ctx_.tx, - *ammSle, - ammAccountID, - amountBalance, - lptAMMBalance, - *amount, - *ePrice, - tfee); + sb, *ammSle, ammAccountID, amountBalance, lptAMMBalance, *amount, *ePrice, tfee); } if (subTxType & tfSingleAsset) { return singleWithdraw( - sb, ctx_.tx, *ammSle, ammAccountID, amountBalance, lptAMMBalance, *amount, tfee); + sb, *ammSle, ammAccountID, amountBalance, lptAMMBalance, *amount, tfee); } if (subTxType & tfLPToken || subTxType & tfWithdrawAll) { return equalWithdrawTokens( sb, - ctx_.tx, *ammSle, ammAccountID, amountBalance, @@ -419,6 +407,16 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (!isTesSuccess(result)) return {result, false}; + if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) + { + if (auto const ter = checkAMMPrecisionLoss( + sb, ammAccountID, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], newLPTokenBalance, j_); + !isTesSuccess(ter)) + { + return {ter, false}; + } + } + auto const res = deleteAMMAccountIfEmpty( sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); // LCOV_EXCL_START @@ -450,7 +448,6 @@ AMMWithdraw::doApply() std::pair AMMWithdraw::withdraw( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -464,7 +461,6 @@ AMMWithdraw::withdraw( STAmount newLPTokenBalance; std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = withdraw( view, - tx, ammSle, ammAccount, accountID_, @@ -485,7 +481,6 @@ AMMWithdraw::withdraw( std::tuple> AMMWithdraw::withdraw( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, AccountID const& account, @@ -609,17 +604,6 @@ AMMWithdraw::withdraw( } } - // this is also called from AMMClawback, but only AMMWithdraw does sponsor - // the new trustline - SLE::pointer sponsorSle; - if (tx[sfAccount] == account) - { - auto sle = getTxReserveSponsor(view, tx); - if (!sle) - return {sle.error(), STAmount{}, STAmount{}, STAmount{}}; // LCOV_EXCL_LINE - sponsorSle = std::move(*sle); - } - // Check the reserve in case a trustline or MPT has to be created bool const enabledFixAmMv12 = view.rules().enabled(fixAMMv1_2); // If seated after a call to sufficientReserve() then MPToken must be @@ -647,23 +631,15 @@ AMMWithdraw::withdraw( return tecINTERNAL; // LCOV_EXCL_LINE auto const balance = (*sleAccount)[sfBalance]->xrp(); - std::uint32_t const count = - ownerCount(view, sponsorSle ? sponsorSle : sleAccount, journal); + std::uint32_t const currentOwnerCount = ownerCount(sleAccount, journal); // See also TrustSet::doApply() and MPTokenAuthorize::authorize() - if (count >= 2) - { - if (auto const ret = checkInsufficientReserve( - view, - tx, - sleAccount, - std::max(priorBalance, balance), - sponsorSle, - 1, - 0, - journal); - !isTesSuccess(ret)) - return ret; - } + XRPAmount const reserve( + (currentOwnerCount < 2) ? XRPAmount(beast::kZero) + : accountReserve(view, sleAccount, journal, 1)); + + auto const balanceAdj = isIssue ? std::max(priorBalance, balance) : priorBalance; + if (balanceAdj < reserve) + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; }; @@ -678,7 +654,7 @@ AMMWithdraw::withdraw( !isTesSuccess(err)) return err; - if (auto const err = checkCreateMPT(view, mptIssue, account, sponsorSle, journal); + if (auto const err = checkCreateMPT(view, mptIssue, account, {}, journal); !isTesSuccess(err)) { return err; @@ -695,13 +671,7 @@ AMMWithdraw::withdraw( // Withdraw amountWithdraw auto res = accountSend( - view, - ammAccount, - account, - amountWithdrawActual, - journal, - sponsorSle, - WaiveTransferFee::Yes); + view, ammAccount, account, amountWithdrawActual, journal, {}, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -720,13 +690,7 @@ AMMWithdraw::withdraw( return {res, STAmount{}, STAmount{}, STAmount{}}; res = accountSend( - view, - ammAccount, - account, - *amount2WithdrawActual, - journal, - sponsorSle, - WaiveTransferFee::Yes); + view, ammAccount, account, *amount2WithdrawActual, journal, {}, WaiveTransferFee::Yes); if (!isTesSuccess(res)) { // LCOV_EXCL_START @@ -771,7 +735,6 @@ adjustLPTokensIn( std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -785,7 +748,6 @@ AMMWithdraw::equalWithdrawTokens( STAmount newLPTokenBalance; std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = equalWithdrawTokens( view, - tx, ammSle, accountID_, ammAccount, @@ -837,7 +799,6 @@ AMMWithdraw::deleteAMMAccountIfEmpty( std::tuple> AMMWithdraw::equalWithdrawTokens( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const account, AccountID const& ammAccount, @@ -860,7 +821,6 @@ AMMWithdraw::equalWithdrawTokens( { return withdraw( view, - tx, ammSle, ammAccount, account, @@ -896,7 +856,6 @@ AMMWithdraw::equalWithdrawTokens( return withdraw( view, - tx, ammSle, ammAccount, account, @@ -949,7 +908,6 @@ AMMWithdraw::equalWithdrawTokens( std::pair AMMWithdraw::equalWithdrawLimit( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -970,7 +928,6 @@ AMMWithdraw::equalWithdrawLimit( { return withdraw( view, - tx, ammSle, ammAccount, amountBalance, @@ -1003,7 +960,6 @@ AMMWithdraw::equalWithdrawLimit( } return withdraw( view, - tx, ammSle, ammAccount, amountBalance, @@ -1022,7 +978,6 @@ AMMWithdraw::equalWithdrawLimit( std::pair AMMWithdraw::singleWithdraw( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1051,7 +1006,6 @@ AMMWithdraw::singleWithdraw( return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, - tx, ammSle, ammAccount, amountBalance, @@ -1075,7 +1029,6 @@ AMMWithdraw::singleWithdraw( std::pair AMMWithdraw::singleWithdrawTokens( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1094,7 +1047,6 @@ AMMWithdraw::singleWithdrawTokens( { return withdraw( view, - tx, ammSle, ammAccount, amountBalance, @@ -1130,7 +1082,6 @@ AMMWithdraw::singleWithdrawTokens( std::pair AMMWithdraw::singleWithdrawEPrice( Sandbox& view, - STTx const& tx, SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, @@ -1178,7 +1129,6 @@ AMMWithdraw::singleWithdrawEPrice( { return withdraw( view, - tx, ammSle, ammAccount, amountBalance, diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index b3cc744332..d2eb101861 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -832,26 +831,23 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) if (!sleCreator) return {tefINTERNAL, false}; - auto const sponsorSle = getTxReserveSponsor(sb, ctx_.tx); - if (!sponsorSle) - return {sponsorSle.error(), false}; // LCOV_EXCL_LINE - - if (auto const ret = checkInsufficientReserve( - sb, ctx_.tx, sleCreator, preFeeBalance_, *sponsorSle, 1, 0, j_); - !isTesSuccess(ret)) { - // If we are here, the signing account had an insufficient reserve - // *prior* to our processing. If something actually crossed, then - // we allow this; otherwise, we just claim a fee. - if (!crossed) - result = tecINSUF_RESERVE_OFFER; - - if (!isTesSuccess(result)) + XRPAmount const reserve = accountReserve(sb, sleCreator, viewJ, 1); + if (preFeeBalance_ < reserve) { - JLOG(j_.debug()) << "final result: " << transToken(result); - } + // If we are here, the signing account had an insufficient reserve + // *prior* to our processing. If something actually crossed, then + // we allow this; otherwise, we just claim a fee. + if (!crossed) + result = tecINSUF_RESERVE_OFFER; - return {result, true}; + if (!isTesSuccess(result)) + { + JLOG(j_.debug()) << "final result: " << transToken(result); + } + + return {result, true}; + } } // We need to place the remainder of the offer into its order book. @@ -870,7 +866,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // Update owner count. - adjustOwnerCount(sb, sleCreator, *sponsorSle, 1, viewJ); + adjustOwnerCount(sb, sleCreator, {}, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.asset()) << " : " << to_string(saTakerGets.asset()) @@ -939,7 +935,6 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) sleOffer->setFlag(lsfSell); if (domainID) sleOffer->setFieldH256(sfDomainID, *domainID); - addSponsorToLedgerEntry(sleOffer, *sponsorSle); // if it's a hybrid offer, set hybrid flag, and create an open dir if (bHybrid) diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp b/src/libxrpl/tx/transactors/did/DIDDelete.cpp index a2f328dec4..1617f880c9 100644 --- a/src/libxrpl/tx/transactors/did/DIDDelete.cpp +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp @@ -50,7 +50,8 @@ DIDDelete::deleteSLE(ApplyView& view, SLE::pointer sle, AccountID const owner, b if (!sleOwner) return tecINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCountObj(view, sleOwner, sle, -1, j); + adjustOwnerCount(view, sleOwner, {}, -1, j); + view.update(sleOwner); // Remove object from ledger view.erase(sle); diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index 094d0548f3..1157167082 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -71,14 +70,13 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tefINTERNAL; // LCOV_EXCL_LINE // Check reserve availability for new object creation - auto const sponsorSle = getTxReserveSponsor(ctx.view(), ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); - if (auto const ret = checkInsufficientReserve( - ctx.view(), ctx.tx, sleAccount, balance, *sponsorSle, 1, 0, ctx.journal); - !isTesSuccess(ret)) - return ret; + { + auto const balance = STAmount((*sleAccount)[sfBalance]).xrp(); + auto const reserve = accountReserve(ctx.view(), sleAccount, ctx.journal, 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + } // Add ledger object to ledger ctx.view().insert(sle); @@ -91,8 +89,7 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner) return tecDIR_FULL; // LCOV_EXCL_LINE (*sle)[sfOwnerNode] = *page; } - adjustOwnerCount(ctx.view(), sleAccount, *sponsorSle, 1, ctx.journal); - addSponsorToLedgerEntry(sle, *sponsorSle); + adjustOwnerCount(ctx.view(), sleAccount, {}, 1, ctx.journal); ctx.view().update(sleAccount); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp index 9de281f4a6..0a8123e110 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp @@ -184,6 +184,10 @@ LoanBrokerDelete::doApply() return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE } + view().erase(brokerPseudoSLE); + + view().erase(broker); + { auto owner = view().peek(keylet::account(accountID_)); if (!owner) @@ -192,14 +196,9 @@ LoanBrokerDelete::doApply() // Decreases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. // LoanBroker object can be sponsored - adjustOwnerCountObj(view(), owner, broker, -1, j_); - - // pseudo-account cannot be sponsored - adjustOwnerCount(view(), owner, {}, -1, j_); + adjustOwnerCount(view(), owner, {}, -2, j_); } - view().erase(brokerPseudoSLE); - view().erase(broker); associateAsset(*broker, vaultAsset); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index 826ff7f6fa..6509eb0ec0 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -237,29 +236,11 @@ LoanBrokerSet::doApply() if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) return ter; // LCOV_EXCL_LINE - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - if (auto const ret = checkInsufficientReserve( - view, tx, owner, preFeeBalance_, {}, *sponsorSle ? 1 : 2, 0, j_); - !isTesSuccess(ret)) - return ret; - - if (*sponsorSle) - { - if (auto const ret = checkInsufficientReserve( - view, tx, owner, preFeeBalance_, *sponsorSle, 1, 0, j_); - !isTesSuccess(ret)) - return ret; - } - // Increases the owner count by two: one for the LoanBroker object, and // one for the pseudo-account. - // Pseudo-account cannot be sponsored - adjustOwnerCount(view, owner, {}, 1, j_); - // LoanBroker object can be sponsored - adjustOwnerCount(view, owner, *sponsorSle, 1, j_); + adjustOwnerCount(view, owner, {}, 2, j_); + if (preFeeBalance_ < accountReserve(view, owner, j_)) + return tecINSUFFICIENT_RESERVE; auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); if (!maybePseudo) @@ -289,8 +270,6 @@ LoanBrokerSet::doApply() if (auto const coverLiq = tx[~sfCoverRateLiquidation]) broker->at(sfCoverRateLiquidation) = *coverLiq; - addSponsorToLedgerEntry(broker, *sponsorSle); - view.insert(broker); associateAsset(*broker, vaultAsset); diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp index bc2842fbb3..37ecc04b09 100644 --- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp @@ -103,6 +103,9 @@ LoanDelete::doApply() if (!view.dirRemove(keylet::ownerDir(borrower), loanSle->at(sfOwnerNode), loanID, false)) return tefBAD_LEDGER; // LCOV_EXCL_LINE + // Delete the Loan object + view.erase(loanSle); + // Decrement the LoanBroker's owner count. // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count @@ -127,10 +130,7 @@ LoanDelete::doApply() } } // Decrement the borrower's owner count - adjustOwnerCountObj(view, borrowerSle, loanSle, -1, j_); - - // Delete the Loan object - view.erase(loanSle); + adjustOwnerCount(view, borrowerSle, {}, -1, j_); // These associations shouldn't do anything, but do them just to be safe associateAsset(*loanSle, vaultAsset); diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 59a762206b..3f38a4f4d3 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -57,6 +57,12 @@ LoanSet::preflight(PreflightContext const& ctx) auto const& tx = ctx.tx; + if (tx.isFieldPresent(sfSponsorFlags) && isReserveSponsored(tx)) + { + JLOG(ctx.j.debug()) << "LoanSet: reserve sponsorship is not allowed."; + return temINVALID_FLAG; + } + // Special case for Batch inner transactions if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) && !tx.isFieldPresent(sfCounterparty)) @@ -513,18 +519,14 @@ LoanSet::doApply() } } - auto const sponsorSle = getTxReserveSponsor(view, tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE + adjustOwnerCount(view, borrowerSle, {}, 1, j_); + { auto const balance = accountID_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp(); - if (auto const ret = - checkInsufficientReserve(view, tx, borrowerSle, balance, *sponsorSle, 1, 0, j_); - !isTesSuccess(ret)) - return ret; + if (balance < accountReserve(view, borrowerSle, j_)) + return tecINSUFFICIENT_RESERVE; } - adjustOwnerCount(view, borrowerSle, *sponsorSle, 1, j_); // Account for the origination fee using two payments // @@ -624,7 +626,6 @@ LoanSet::doApply() loan->at(sfPreviousPaymentDueDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; - addSponsorToLedgerEntry(loan, *sponsorSle); view.insert(loan); // Update the balances in the vault diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 2298e2a8a6..f37a3ccfa0 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -374,12 +373,7 @@ NFTokenAcceptOffer::transferNFToken( std::uint32_t const buyerOwnerCountBefore = sleBuyer->getFieldU32(sfOwnerCount); - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - auto const insertRet = - nft::insertToken(view(), ctx_.tx, buyer, *sponsorSle, std::move(tokenAndPage->token)); + 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. @@ -399,13 +393,8 @@ NFTokenAcceptOffer::transferNFToken( auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount); if (buyerOwnerCountAfter > buyerOwnerCountBefore) { - SLE::const_pointer buyerSponsorSle; - if (accountID_ == buyer) - buyerSponsorSle = *sponsorSle; - if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, sleBuyer, buyerBalance, buyerSponsorSle, 0, 0, j_); - !isTesSuccess(ret)) - return ret; + if (buyerBalance < accountReserve(view(), sleBuyer, j_)) + return tecINSUFFICIENT_RESERVE; } } diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index 11a2f38872..1948f3803d 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -77,7 +77,6 @@ NFTokenCreateOffer::doApply() // Use implementation shared with NFTokenMint return nft::tokenOfferCreateApply( view(), - ctx_.tx, ctx_.tx[sfAccount], ctx_.tx[sfAmount], ctx_.tx[~sfDestination], diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp index 4270f50c59..9a158aafca 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -306,12 +305,7 @@ NFTokenMint::doApply() object.setFieldVL(sfURI, *uri); }); - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - - if (TER const ret = - nft::insertToken(ctx_.view(), ctx_.tx, accountID_, *sponsorSle, std::move(newToken)); + if (TER const ret = nft::insertToken(ctx_.view(), accountID_, std::move(newToken)); !isTesSuccess(ret)) return ret; @@ -322,7 +316,6 @@ NFTokenMint::doApply() // because a Mint is only allowed to create a sell offer. if (TER const ter = nft::tokenOfferCreateApply( view(), - ctx_.tx, ctx_.tx[sfAccount], ctx_.tx[sfAmount], ctx_.tx[~sfDestination], @@ -339,21 +332,15 @@ NFTokenMint::doApply() // allows NFTs to be added to the page (and burn fees) without // requiring the reserve to be met each time. The reserve is // only managed when a new NFT page or sell offer is added. - if (auto const ownerCountAfter = - view().read(keylet::account(accountID_))->getFieldU32(sfOwnerCount); + auto const sleAccount = view().read(keylet::account(accountID_)); + if (!sleAccount) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (auto const ownerCountAfter = sleAccount->getFieldU32(sfOwnerCount); ownerCountAfter > ownerCountBefore) { - if (auto const ret = checkInsufficientReserve( - ctx_.view(), - ctx_.tx, - view().read(keylet::account(accountID_)), - preFeeBalance_, - *sponsorSle, - 0, - 0, - j_); - !isTesSuccess(ret)) - return ret; + if (preFeeBalance_ < accountReserve(view(), sleAccount, j_)) + return tecINSUFFICIENT_RESERVE; } return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp index 6862616eb9..823a69b101 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp @@ -69,7 +69,7 @@ OracleDelete::deleteOracle( return tecINTERNAL; // LCOV_EXCL_LINE auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1; - adjustOwnerCountObj(view, sleOwner, sle, count, j); + adjustOwnerCount(view, sleOwner, {}, count, j); view.erase(sle); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp index 779d312e3b..dd5bcbb117 100644 --- a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -154,18 +153,7 @@ OracleSet::preclaim(PreclaimContext const& ctx) auto const oldCount = calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size()); auto const newCount = calculateOracleReserve(pairs.size()); - // if different sponsors, check with newCount - auto const currentSponsor = getLedgerEntryReserveSponsorAccountID(sle); - auto const newSponsor = getTxReserveSponsorAccountID(ctx.tx); - if ((!currentSponsor && !newSponsor) || - (currentSponsor && newSponsor && *currentSponsor == *newSponsor)) - { - adjustReserve = newCount - oldCount; - } - else - { - adjustReserve = newCount; - } + adjustReserve = newCount - oldCount; } else { @@ -181,18 +169,27 @@ OracleSet::preclaim(PreclaimContext const& ctx) if (pairs.size() > kMaxOracleDataSeries) return tecARRAY_TOO_LARGE; + auto const reserve = accountReserve(ctx.view, sleSetter, ctx.j, adjustReserve); auto const& balance = sleSetter->getFieldAmount(sfBalance); - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sleSetter, balance, *sponsorSle, adjustReserve, 0, ctx.j); - !isTesSuccess(ret)) - return ret; + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; return tesSUCCESS; } +static bool +adjustOwnerCount(ApplyContext& ctx, int count) +{ + if (auto const sleAccount = ctx.view().peek(keylet::account(ctx.tx[sfAccount]))) + { + adjustOwnerCount(ctx.view(), sleAccount, {}, count, ctx.journal); + return true; + } + + return false; // LCOV_EXCL_LINE +} + static void setPriceDataInnerObjTemplate(STObject& obj) { @@ -272,34 +269,9 @@ OracleSet::doApply() auto const newCount = calculateOracleReserve(pairs.size()); int32_t const adjust = newCount - oldCount; - auto const accountSle = ctx_.view().peek(keylet::account(ctx_.tx[sfAccount])); - if (!accountSle) + if (adjust != 0 && !adjustOwnerCount(ctx_, adjust)) return tefINTERNAL; // LCOV_EXCL_LINE - if (adjust > 0) - { - // To continue receiving sponsorship from the same account after the - // OwnerCount increases from 1 to 2, it is necessary to sign with - // the sponsor decrease current sponsored owner count. - // Otherwise, the sponsorship will be deleted. - - auto const newSponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx); - if (!newSponsorSle) - return newSponsorSle.error(); // LCOV_EXCL_LINE - - // decrease current sponsored owner count - adjustOwnerCountObj(ctx_.view(), accountSle, sle, -oldCount, ctx_.journal); - removeSponsorFromLedgerEntry(sle); - // increase new owner count - adjustOwnerCount(ctx_.view(), accountSle, *newSponsorSle, newCount, ctx_.journal); - addSponsorToLedgerEntry(sle, *newSponsorSle); - } - else if (adjust < 0) - { - // decrease owner count - adjustOwnerCountObj(ctx_.view(), accountSle, sle, adjust, ctx_.journal); - } - ctx_.view().update(sle); } else @@ -347,16 +319,9 @@ OracleSet::doApply() (*sle)[sfOwnerNode] = *page; auto const count = calculateOracleReserve(series.size()); - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - auto const accountSle = ctx_.view().peek(keylet::account(ctx_.tx[sfAccount])); - if (!accountSle) + if (!adjustOwnerCount(ctx_, count)) return tefINTERNAL; // LCOV_EXCL_LINE - adjustOwnerCount(ctx_.view(), accountSle, *sponsorSle, count, ctx_.journal); - addSponsorToLedgerEntry(sle, *sponsorSle); - ctx_.view().insert(sle); } diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 08632158ac..6eab8b45c6 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -132,6 +130,9 @@ Payment::preflight(PreflightContext const& ctx) if (tx.isFlag(tfNoRippleDirect) || tx.isFlag(tfPartialPayment) || tx.isFlag(tfLimitQuality)) return temINVALID_FLAG; + if (tx.isFieldPresent(sfSendMax) || tx.isFieldPresent(sfPaths)) + return temINVALID; + if (!dstAmount.native()) return temBAD_AMOUNT; } @@ -287,38 +288,24 @@ Payment::preflight(PreflightContext const& ctx) } NotTEC -Payment::checkPermission(ReadView const& view, STTx const& tx) +Payment::checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) { - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttPAYMENT, granularPermissions); - auto const& dstAmount = tx.getFieldAmount(sfAmount); auto const& amountAsset = dstAmount.asset(); // Granular permissions are only valid for direct payments. - if ((tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) || - tx.isFieldPresent(sfPaths)) + if (tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) return terNO_DELEGATE_PERMISSION; // PaymentMint and PaymentBurn apply to both IOU and MPT direct payments. - if (granularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && + if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && amountAsset.getIssuer() == tx[sfAccount]) return tesSUCCESS; - if (granularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && + if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && amountAsset.getIssuer() == tx[sfDestination]) return tesSUCCESS; @@ -653,7 +640,7 @@ Payment::doApply() // In a delegated payment, the fee payer is the delegated account, // not the source account (accountID_). - bool const accountIsPayer = (ctx_.tx.getFeePayer() == accountID_); + bool const accountIsPayer = (ctx_.tx.getInitiator() == accountID_); // preFeeBalance_ is the balance on the source account (accountID_) BEFORE the fees // were charged. If source account is the fee payer, it must also cover the fee. diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index b1fe5e24bc..b8118bc49f 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -42,6 +42,9 @@ PaymentChannelClaim::getFlagsMask(PreflightContext const&) NotTEC PaymentChannelClaim::preflight(PreflightContext const& ctx) { + if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero) + return temMALFORMED; + auto const bal = ctx.tx[~sfBalance]; if (bal && (!isXRP(*bal) || *bal <= beast::kZero)) return temBAD_AMOUNT; @@ -116,12 +119,10 @@ PaymentChannelClaim::doApply() AccountID const txAccount = ctx_.tx[sfAccount]; auto const curExpiration = (*slep)[~sfExpiration]; + if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) || + isChannelExpired(ctx_.view(), curExpiration)) { - auto const cancelAfter = (*slep)[~sfCancelAfter]; - auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); - if ((cancelAfter && closeTime >= *cancelAfter) || - (curExpiration && closeTime >= *curExpiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); } if (txAccount != src && txAccount != dst) @@ -134,13 +135,19 @@ PaymentChannelClaim::doApply() auto const reqBalance = ctx_.tx[sfBalance].xrp(); if (txAccount == dst && !ctx_.tx[~sfSignature]) - return temBAD_SIGNATURE; + { + return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} + : TER{temBAD_SIGNATURE}; + } if (ctx_.tx[~sfSignature]) { PublicKey const pk((*slep)[sfPublicKey]); if (ctx_.tx[sfPublicKey] != pk) - return temBAD_SIGNER; + { + return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} + : TER{temBAD_SIGNER}; + } } if (reqBalance > chanFunds) @@ -184,9 +191,10 @@ PaymentChannelClaim::doApply() if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); - auto const settleExpiration = - ctx_.view().header().parentCloseTime.time_since_epoch().count() + - (*slep)[sfSettleDelay]; + auto const settleExpiration = saturatingAdd( + ctx_.view().rules(), + ctx_.view().header().parentCloseTime.time_since_epoch().count(), + (*slep)[sfSettleDelay]); if (!curExpiration || *curExpiration > settleExpiration) { diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index 04d8e6280b..a6c1d176bb 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -78,19 +78,16 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; // Check reserve and funds availability + if (!ctx.view.rules().enabled(featureSponsor)) { auto const balance = (*sle)[sfBalance]; - auto const sponsorSle = getTxReserveSponsor(ctx.view, ctx.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = - checkInsufficientReserve(ctx.view, ctx.tx, sle, balance, *sponsorSle, 1, 0, ctx.j); - !isTesSuccess(ret)) - return ret; + auto const fees = ctx.view.fees(); + auto const reserve = fees.reserve + fees.increment * ((*sle)[sfOwnerCount] + 1); - if (auto const ret = checkInsufficientReserve( - ctx.view, ctx.tx, sle, balance - ctx.tx[sfAmount], *sponsorSle, 1, 0, ctx.j); - !isTesSuccess(ret)) + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + if (balance < reserve + ctx.tx[sfAmount]) return tecUNFUNDED; } @@ -137,6 +134,28 @@ PaymentChannelCreate::doApply() return tecEXPIRED; } + if (ctx_.view().rules().enabled(featureSponsor)) + { + auto const sponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx); + if (!sponsorSle) + return sponsorSle.error(); + if (auto const ret = checkInsufficientReserve( + ctx_.view(), ctx_.tx, sle, STAmount{preFeeBalance_}, *sponsorSle, 1, 0, j_); + !isTesSuccess(ret)) + return ret; + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + ctx_.tx, + sle, + STAmount{preFeeBalance_ - ctx_.tx[sfAmount].xrp()}, + {}, + 1, + 0, + j_); + !isTesSuccess(ret)) + return tecUNFUNDED; + } + auto const dst = ctx_.tx[sfDestination]; // Create PayChan in ledger. diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 1a19c466ad..db14a9b06c 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,9 @@ PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx) NotTEC PaymentChannelFund::preflight(PreflightContext const& ctx) { + if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero) + return temMALFORMED; + if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero)) return temBAD_AMOUNT; @@ -47,13 +51,12 @@ PaymentChannelFund::doApply() AccountID const src = (*slep)[sfAccount]; auto const txAccount = ctx_.tx[sfAccount]; - auto const expiration = (*slep)[~sfExpiration]; + auto const curExpiration = (*slep)[~sfExpiration]; + if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) || + isChannelExpired(ctx_.view(), curExpiration)) { - auto const cancelAfter = (*slep)[~sfCancelAfter]; - auto const closeTime = ctx_.view().header().parentCloseTime.time_since_epoch().count(); - if ((cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)) - return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); + return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View")); } if (src != txAccount) @@ -62,16 +65,21 @@ PaymentChannelFund::doApply() return tecNO_PERMISSION; } - if (auto extend = ctx_.tx[~sfExpiration]) + if (auto newExpiration = ctx_.tx[~sfExpiration]) { - auto minExpiration = ctx_.view().header().parentCloseTime.time_since_epoch().count() + - (*slep)[sfSettleDelay]; - if (expiration && *expiration < minExpiration) - minExpiration = *expiration; + auto minExpiration = saturatingAdd( + ctx_.view().rules(), + ctx_.view().header().parentCloseTime.time_since_epoch().count(), + (*slep)[sfSettleDelay]); + if (curExpiration && *curExpiration < minExpiration) + minExpiration = *curExpiration; - if (*extend < minExpiration) - return temBAD_EXPIRATION; - (*slep)[~sfExpiration] = *extend; + if (*newExpiration < minExpiration) + { + return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION} + : TER{temBAD_EXPIRATION}; + } + (*slep)[~sfExpiration] = *newExpiration; ctx_.view().update(slep); } diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp index fc81d914d8..b02073f981 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp @@ -65,7 +65,7 @@ PermissionedDomainDelete::doApply() XRPL_ASSERT( ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0, "xrpl::PermissionedDomainDelete::doApply : nonzero owner count"); - adjustOwnerCountObj(view(), ownerSle, slePd, -1, ctx_.journal); + adjustOwnerCount(view(), ownerSle, {}, -1, ctx_.journal); view().erase(slePd); return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp index d485ec22e9..658c59f984 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -107,13 +106,9 @@ PermissionedDomainSet::doApply() // Create new permissioned domain. // Check reserve availability for new object creation auto const balance = STAmount((*ownerSle)[sfBalance]).xrp(); - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - ctx_.view(), ctx_.tx, ownerSle, balance, *sponsorSle, 1, 0, j_); - !isTesSuccess(ret)) - return ret; + auto const reserve = accountReserve(ctx_.view(), ownerSle, ctx_.journal, 1); + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; bool const fixEnabled = view().rules().enabled(fixCleanup3_1_3); auto const seq = fixEnabled ? ctx_.tx.getSeqValue() : ctx_.tx.getFieldU32(sfSequence); @@ -130,8 +125,7 @@ PermissionedDomainSet::doApply() slePd->setFieldU64(sfOwnerNode, *page); // If we succeeded, the new entry counts against the creator's reserve. - adjustOwnerCount(view(), ownerSle, *sponsorSle, 1, ctx_.journal); - addSponsorToLedgerEntry(slePd, *sponsorSle); + adjustOwnerCount(view(), ownerSle, {}, 1, ctx_.journal); view().insert(slePd); } diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index adddd9de7c..279e2b1ec1 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -214,8 +215,7 @@ Batch::preflight(PreflightContext const& ctx) if (ctx.tx.isFieldPresent(sfSponsorFlags)) { - auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags); - if ((sponsorFlags & spfSponsorReserve) != 0u) + if (isReserveSponsored(ctx.tx)) { JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" << "spfSponsorReserve is not allowed on outer Batch."; diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index 3d301ebd77..e8483998d9 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -76,13 +75,8 @@ TicketCreate::doApply() // check the starting balance because we want to allow dipping into the // reserve to pay fees. std::uint32_t const ticketCount = ctx_.tx[sfTicketCount]; - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (auto const ret = checkInsufficientReserve( - view(), ctx_.tx, sleAccountRoot, preFeeBalance_, *sponsorSle, ticketCount, 0, j_); - !isTesSuccess(ret)) - return ret; + if (preFeeBalance_ < accountReserve(view(), sleAccountRoot, j_, ticketCount)) + return tecINSUFFICIENT_RESERVE; beast::Journal const viewJ{ctx_.registry.get().getJournal("View")}; @@ -119,7 +113,6 @@ TicketCreate::doApply() return tecDIR_FULL; // LCOV_EXCL_LINE sleTicket->setFieldU64(sfOwnerNode, *page); - addSponsorToLedgerEntry(sleTicket, *sponsorSle); } // Update the record of the number of Tickets this account owns. @@ -128,7 +121,7 @@ TicketCreate::doApply() sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); // Every added Ticket counts against the creator's reserve. - adjustOwnerCount(view(), sleAccountRoot, *sponsorSle, ticketCount, viewJ); + adjustOwnerCount(view(), accountID_, {}, ticketCount, viewJ); // TicketCreate is the only transaction that can cause an account root's // Sequence field to increase by more than one. October 2018. diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index 9b25531161..1cb12709d5 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -16,14 +15,12 @@ #include #include #include -#include #include #include #include #include #include -#include namespace xrpl { @@ -41,35 +38,34 @@ MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) return tfMPTokenIssuanceSetMask; } -// Maps set/clear mutable flags in an MPTokenIssuanceSet transaction to the -// corresponding ledger mutable flags that control whether the change is -// allowed. +// Maps each MPTokenIssuanceSet MutableFlags to the corresponding mutable +// flag and the target ledger flag to mutate. struct MPTMutabilityFlags { std::uint32_t setFlag; - std::uint32_t clearFlag; - std::uint32_t canMutateFlag; + std::uint32_t canEnableFlag; + std::uint32_t ledgerFlag; }; static constexpr std::array kMptMutabilityFlags = { {{.setFlag = tmfMPTSetCanLock, - .clearFlag = tmfMPTClearCanLock, - .canMutateFlag = lsmfMPTCanMutateCanLock}, + .canEnableFlag = lsmfMPTCanEnableCanLock, + .ledgerFlag = lsfMPTCanLock}, {.setFlag = tmfMPTSetRequireAuth, - .clearFlag = tmfMPTClearRequireAuth, - .canMutateFlag = lsmfMPTCanMutateRequireAuth}, + .canEnableFlag = lsmfMPTCanEnableRequireAuth, + .ledgerFlag = lsfMPTRequireAuth}, {.setFlag = tmfMPTSetCanEscrow, - .clearFlag = tmfMPTClearCanEscrow, - .canMutateFlag = lsmfMPTCanMutateCanEscrow}, + .canEnableFlag = lsmfMPTCanEnableCanEscrow, + .ledgerFlag = lsfMPTCanEscrow}, {.setFlag = tmfMPTSetCanTrade, - .clearFlag = tmfMPTClearCanTrade, - .canMutateFlag = lsmfMPTCanMutateCanTrade}, + .canEnableFlag = lsmfMPTCanEnableCanTrade, + .ledgerFlag = lsfMPTCanTrade}, {.setFlag = tmfMPTSetCanTransfer, - .clearFlag = tmfMPTClearCanTransfer, - .canMutateFlag = lsmfMPTCanMutateCanTransfer}, + .canEnableFlag = lsmfMPTCanEnableCanTransfer, + .ledgerFlag = lsfMPTCanTransfer}, {.setFlag = tmfMPTSetCanClawback, - .clearFlag = tmfMPTClearCanClawback, - .canMutateFlag = lsmfMPTCanMutateCanClawback}}}; + .canEnableFlag = lsmfMPTCanEnableCanClawback, + .ledgerFlag = lsfMPTCanClawback}}}; NotTEC MPTokenIssuanceSet::preflight(PreflightContext const& ctx) @@ -121,56 +117,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { if ((*mutableFlags == 0u) || ((*mutableFlags & tmfMPTokenIssuanceSetMutableMask) != 0u)) return temINVALID_FLAG; - - // Can not set and clear the same flag - if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags](auto const& f) { - return (*mutableFlags & f.setFlag) && (*mutableFlags & f.clearFlag); - })) - return temINVALID_FLAG; - - // Trying to set a non-zero TransferFee and clear MPTCanTransfer - // in the same transaction is not allowed. - if ((transferFee.value_or(0) != 0u) && ((*mutableFlags & tmfMPTClearCanTransfer) != 0u)) - return temMALFORMED; } } return tesSUCCESS; } -NotTEC -MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) -{ - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - // this is added in case more flags will be added for MPTokenIssuanceSet - // in the future. Currently unreachable. - if ((tx.getFlags() & tfMPTokenIssuanceSetMask) != 0u) - return terNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); - - if (tx.isFlag(tfMPTLock) && !granularPermissions.contains(MPTokenIssuanceLock)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFlag(tfMPTUnlock) && !granularPermissions.contains(MPTokenIssuanceUnlock)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) { @@ -232,16 +184,9 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if (auto const mutableFlags = ctx.tx[~sfMutableFlags]) { if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags, &isMutableFlag](auto const& f) { - return !isMutableFlag(f.canMutateFlag) && - ((*mutableFlags & (f.setFlag | f.clearFlag))); + return !isMutableFlag(f.canEnableFlag) && ((*mutableFlags & f.setFlag) != 0u); })) return tecNO_PERMISSION; - - // Clearing lsfMPTRequireAuth is invalid when the issuance already has - // a DomainID set, because a DomainID requires RequireAuth to be active. - if ((*mutableFlags & tmfMPTClearRequireAuth) != 0u && - sleMptIssuance->isFieldPresent(sfDomainID)) - return tecNO_PERMISSION; } if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata)) @@ -301,19 +246,8 @@ MPTokenIssuanceSet::doApply() { if ((mutableFlags & f.setFlag) != 0u) { - flagsOut |= f.canMutateFlag; + flagsOut |= f.ledgerFlag; } - else if ((mutableFlags & f.clearFlag) != 0u) - { - flagsOut &= ~f.canMutateFlag; - } - } - - if ((mutableFlags & tmfMPTClearCanTransfer) != 0u) - { - // If the lsfMPTCanTransfer flag is being cleared, then also clear - // the TransferFee field. - sle->makeFieldAbsent(sfTransferFee); } } diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 5f00bfb720..6961b40bd1 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -125,51 +123,21 @@ TrustSet::preflight(PreflightContext const& ctx) } NotTEC -TrustSet::checkPermission(ReadView const& view, STTx const& tx) +TrustSet::checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) { - auto const delegate = tx[~sfDelegate]; - if (!delegate) - return tesSUCCESS; - - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - - if (!sle) - return terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - // Currently we only support TrustlineAuthorize, TrustlineFreeze and - // TrustlineUnfreeze granular permission. Setting other flags returns - // error. - if ((tx.getFlags() & tfTrustSetPermissionMask) != 0u) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut)) - return terNO_DELEGATE_PERMISSION; - auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount); auto const sleRippleState = view.read( keylet::line( tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get().currency)); - // if the trustline does not exist, granular permissions are - // not allowed to create trustline + // granular permissions are not allowed to create a trustline if (!sleRippleState) return terNO_DELEGATE_PERMISSION; - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttTRUST_SET, granularPermissions); - - if (tx.isFlag(tfSetfAuth) && !granularPermissions.contains(TrustlineAuthorize)) - return terNO_DELEGATE_PERMISSION; - if (tx.isFlag(tfSetFreeze) && !granularPermissions.contains(TrustlineFreeze)) - return terNO_DELEGATE_PERMISSION; - if (tx.isFlag(tfClearFreeze) && !granularPermissions.contains(TrustlineUnfreeze)) - return terNO_DELEGATE_PERMISSION; - - // updating LimitAmount is not allowed only with granular permissions, + // updating LimitAmount is not allowed with granular permissions, // unless there's a new granular permission for this in the future. auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer() ? sleRippleState->getFieldAmount(sfHighLimit) @@ -364,11 +332,11 @@ TrustSet::doApply() if (!sponsorSle) return sponsorSle.error(); // LCOV_EXCL_LINE - std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_); + std::uint32_t const uOwnerCount = ownerCount(*sponsorSle ? *sponsorSle : sle, j_); - bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx); - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. + // The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor. + // With any sponsor on the tx, the sponsor must cover the reserve (via balance or + // prefunded budget), so the reserve check always runs. bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle; std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); @@ -572,7 +540,7 @@ TrustSet::doApply() // calling adjustOwnerCount(). if (auto const ret = checkInsufficientReserve( view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + *sponsorSle && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for low account. @@ -601,7 +569,7 @@ TrustSet::doApply() // calling adjustOwnerCount(). if (auto const ret = checkInsufficientReserve( view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + *sponsorSle && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for high account. diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 7e4a8a8156..711bf275d7 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -157,28 +156,9 @@ VaultCreate::doApply() if (auto ter = dirLink(view(), accountID_, vault)) return ter; // We will create Vault and PseudoAccount, hence increase OwnerCount by 2 - auto const sponsorSle = getTxReserveSponsor(view(), tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - if (!ctx_.view().rules().enabled(featureSponsor)) - { - adjustOwnerCount(view(), owner, *sponsorSle, 2, j_); - addSponsorToLedgerEntry(vault, *sponsorSle); - if (auto const ret = - checkInsufficientReserve(view(), tx, owner, preFeeBalance_, *sponsorSle, 0, 0, j_); - !isTesSuccess(ret)) - return ret; - } - else - { - // after Sponsor Amendment, check insufficient reserve first - if (auto const ret = - checkInsufficientReserve(view(), tx, owner, preFeeBalance_, *sponsorSle, 2, 0, j_); - !isTesSuccess(ret)) - return ret; - adjustOwnerCount(view(), owner, *sponsorSle, 2, j_); - addSponsorToLedgerEntry(vault, *sponsorSle); - } + adjustOwnerCount(view(), owner, {}, 2, j_); + if (preFeeBalance_ < accountReserve(view(), owner, j_)) + return tecINSUFFICIENT_RESERVE; auto maybePseudo = createPseudoAccount(view(), vault->key(), sfVaultID); if (!maybePseudo) diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 3ccb9498c7..8df8e73f12 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -204,7 +204,7 @@ VaultDelete::doApply() } // We are destroying Vault and PseudoAccount, hence decrease by 2 - adjustOwnerCountObj(view(), owner, vault, -2, j_); + adjustOwnerCount(view(), owner, {}, -2, j_); // Destroy the vault. view().erase(vault); diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index 9707a923fc..89205266c2 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -343,19 +342,9 @@ VaultDeposit::doApply() } } - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE - // Transfer shares from vault to depositor. if (auto const ter = accountSend( - view(), - vaultAccount, - accountID_, - sharesCreated, - j_, - *sponsorSle, - WaiveTransferFee::Yes); + view(), vaultAccount, accountID_, sharesCreated, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 59f0ce0a2c..4ac7e57a6d 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -326,19 +325,10 @@ VaultWithdraw::doApply() view().update(vault); auto const& vaultAccount = vault->at(sfAccount); - auto const sponsorSle = getTxReserveSponsor(view(), ctx_.tx); - if (!sponsorSle) - return sponsorSle.error(); // LCOV_EXCL_LINE // Transfer shares from depositor to vault. if (auto const ter = accountSend( - view(), - accountID_, - vaultAccount, - sharesRedeemed, - j_, - *sponsorSle, - WaiveTransferFee::Yes); + view(), accountID_, vaultAccount, sharesRedeemed, j_, {}, WaiveTransferFee::Yes); !isTesSuccess(ter)) return ter; diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 9683e8ac17..ba416d8192 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2486,8 +2486,17 @@ class AMMClawback_test : public beast::unit_test::Suite else if (!features[fixAMMClawbackRounding]) { // sqrt(amount * amount2) >= LPTokens and exceeds the allowed - // tolerance - env(amm::ammClawback(gw, alice, usd, eur, usd(1)), Ter(tecINVARIANT_FAILED)); + // tolerance. + // With fixCleanup3_3_0 this is caught in the transaction layer; + // without it the invariant checker fires instead. + if (features[fixCleanup3_3_0]) + { + env(amm::ammClawback(gw, alice, usd, eur, usd(1)), Ter(tecPRECISION_LOSS)); + } + else + { + env(amm::ammClawback(gw, alice, usd, eur, usd(1)), Ter(tecINVARIANT_FAILED)); + } BEAST_EXPECT(amm.ammExists()); } else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding]) @@ -2514,6 +2523,11 @@ class AMMClawback_test : public beast::unit_test::Suite testFeatureDisabled(all - featureAMMClawback); for (auto const& features : {all - fixAMMv1_3 - fixAMMClawbackRounding - featureMPTokensV2, + // fixAMMv1_3 on, fixAMMClawbackRounding off, fixCleanup3_3_0 off: + // precision loss caught by invariant checker -> tecINVARIANT_FAILED + all - fixAMMClawbackRounding - fixCleanup3_3_0 - featureMPTokensV2, + // fixAMMv1_3 on, fixAMMClawbackRounding off, fixCleanup3_3_0 on: + // precision loss caught in transaction layer -> tecPRECISION_LOSS all - fixAMMClawbackRounding - featureMPTokensV2, all - featureMPTokensV2, all}) diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 97a0f8e9c6..a8539c125b 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -1006,13 +1006,7 @@ private: std::nullopt, std::nullopt, std::nullopt, - // After the Sponsor Amendment, it will result in tesSUCCESS - // if the current XRP == balance the required XRP balance calculated from the - // reserve. - // Before the Amendment, it will result in tecINSUF_RESERVE_LINE - // if the current XRP == balance the required XRP balance calculated from the - // reserve. - features[featureSponsor] ? Ter(tesSUCCESS) : Ter(tecINSUF_RESERVE_LINE)); + Ter(tecINSUF_RESERVE_LINE)); } // Invalid min @@ -2246,7 +2240,9 @@ private: .err = Ter(tecNO_AUTH)}); } - // MPTCanTransfer is not set and the account is not the issuer of MPT + // MPTCanTransfer is not set and the account is not the issuer of MPT. + // The issuer can create the AMM, and an existing LP token holder can + // still withdraw. { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); @@ -2256,15 +2252,17 @@ private: .issuer = gw_, .holders = {alice_}, .pay = 30'000, - .flags = kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer, + .flags = tfMPTCanTrade, .authHolder = true}); AMM amm(env, gw_, XRP(10'000), btc(10'000)); - amm.deposit(DepositArg{.account = alice_, .asset1In = XRP(200), .asset2In = btc(200)}); - // Allow to withdraw if transfer is disabled - btc.set({.mutableFlags = tmfMPTClearCanTransfer}); + auto const lpIssue = amm.lptIssue(); + env.trust(STAmount{lpIssue, 20'000'000}, alice_); + env.close(); + env(pay(gw_, alice_, LPToken(1'000'000).tokens(lpIssue))); + env.close(); + amm.withdraw( WithdrawArg{.account = alice_, .asset1Out = btc(100), .assets = {{XRP, btc}}}); } @@ -7089,7 +7087,6 @@ private: testInvalidInstance(); testInvalidDeposit(all); testInvalidDeposit(all - featureAMMClawback); - testInvalidDeposit(all - featureSponsor); testDeposit(); testInvalidWithdraw(); testWithdraw(); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 1b54c2aab9..b01d58ddff 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -1842,8 +1842,18 @@ private: // are rounded to all LP tokens. testAMM( [&](AMM& ammAlice, Env& env) { - auto const err = - env.enabled(fixAMMv1_3) ? Ter(tecINVARIANT_FAILED) : Ter(tecAMM_BALANCE); + // Without fixAMMv1_3: sub-method returns tecAMM_BALANCE early. + // With fixAMMv1_3 but without fixCleanup3_3_0: sub-method succeeds + // but invariant check catches the precision violation. + // With fixCleanup3_3_0: caught in the transaction layer before + // the invariant checker runs. + auto const err = [&] { + if (!env.enabled(fixAMMv1_3)) + return Ter(tecAMM_BALANCE); + if (env.enabled(fixCleanup3_3_0)) + return Ter(tecPRECISION_LOSS); + return Ter(tecINVARIANT_FAILED); + }(); ammAlice.withdraw( alice_, STAmount{USD, UINT64_C(9'999'999999999999), -12}, @@ -1851,7 +1861,7 @@ private: std::nullopt, err); }, - {.features = {all, all - fixAMMv1_3}, .noLog = true}); + {.features = {all, all - fixAMMv1_3, all - fixCleanup3_3_0}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { diff --git a/src/test/app/AccountSet_test.cpp b/src/test/app/AccountSet_test.cpp index 3f98e3aaac..52fd28f289 100644 --- a/src/test/app/AccountSet_test.cpp +++ b/src/test/app/AccountSet_test.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -12,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -431,10 +429,8 @@ public: env.close(); // Because we're hacking the ledger we need the account to have - // non-zero sfMintedNFTokens, sfBurnedNFTokens, - // sfSponsoredOwnerCount, sfSponsoringOwnerCount, - // sfSponsoringAccountCount fields. This prevents an exception when - // the AccountRoot template is applied. + // non-zero sfMintedNFTokens and sfBurnedNFTokens fields. This + // prevents an exception when the AccountRoot template is applied. { uint256 const nftId0{token::getNextID(env, gw, 0u)}; env(token::mint(gw, 0u)); @@ -442,23 +438,6 @@ public: env(token::burn(gw, nftId0)); env.close(); - - env(did::set(gw), - did::Uri("uri"), - sponsor::As(alice, spfSponsorReserve), - Sig(sfSponsorSignature, alice)); - env.close(); - - env(did::set(alice), - did::Uri("uri"), - sponsor::As(gw, spfSponsorReserve), - Sig(sfSponsorSignature, gw)); - env.close(); - - env(sponsor::transfer(alice, tfSponsorshipCreate), - sponsor::As(gw, spfSponsorReserve), - Sig(sfSponsorSignature, gw)); - env.close(); } // Note that we're bypassing almost all of the ledger's safety @@ -472,7 +451,7 @@ public: // We'll insert a replacement for the account root // with the higher (currently invalid) transfer rate. - auto replacement = std::make_shared(*sle, sle->key()); + auto replacement = std::make_shared(*sle); (*replacement)[sfTransferRate] = static_cast(transferRate * QUALITY_ONE); view.rawReplace(replacement); diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 8c16cc9b04..0779d410e2 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -5,8 +5,11 @@ #include #include #include +#include #include +#include #include +#include #include #include #include @@ -22,7 +25,9 @@ #include #include #include +#include +#include #include #include #include @@ -34,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +48,7 @@ #include #include #include +#include #include #include @@ -1064,6 +1071,93 @@ class Delegate_test : public beast::unit_test::Suite } } + // PaymentMint/PaymentBurn with sfSendMax of the same asset is allowed, + // same-asset SendMax is still a direct payment, not cross-currency. + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + + // sfSendMax with same asset as sfAmount, still a direct payment + env(pay(gw, alice, usd(50)), Sendmax(usd(50)), delegate::As(bob)); + env.require(Balance(alice, usd(50))); + + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + env(pay(alice, gw, usd(30)), Sendmax(usd(30)), delegate::As(bob)); + env.require(Balance(alice, usd(20))); + } + + // Test invalid fields or flags not allowed in granular permission template + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + env(delegate::set(gw, bob, {"PaymentMint"})); + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + // sfDeliverMin (with tfPartialPayment) is not in the PaymentMint + // or PaymentBurn template. + env(pay(gw, alice, usd(100)), + DeliverMin(usd(50)), + Txflags(tfPartialPayment), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, usd(50)), + DeliverMin(usd(25)), + Txflags(tfPartialPayment), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + + // sfDomainID is not in the PaymentMint or PaymentBurn template. + env(pay(gw, alice, usd(100)), + Domain(uint256{1}), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, usd(50)), + Domain(uint256{1}), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // Delegate account holds no granular permissions for the tx type: + // getGranularPermission returns empty set. + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + // Bob holds only an AccountSet granular permission. + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + // Payment has granular permissions defined in permissions.macro, + // but bob only holds AccountSet's granular permission, + // getGranularPermission returns empty. + env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + // PaymentMint and PaymentBurn for MPT { std::string logs; @@ -1120,6 +1214,40 @@ class Delegate_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100)); } } + + // Verify granular permissions of different tx types in the same SLE are scoped + // correctly. AccountSet permissions don't apply to Payment and vice versa + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + // Alice granted bob with both AccountDomainSet and PaymentMint. + env(delegate::set(alice, bob, {"AccountDomainSet", "PaymentMint"})); + env.close(); + + // PaymentMint fails at granular semantic check because alice is not the issuer. + env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // AccountDomainSet applies correctly to AccountSet + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // gw gives bob PaymentMint and bob can mint on gw's behalf + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + env(pay(gw, alice, usd(50)), delegate::As(bob)); + env.require(Balance(alice, usd(50))); + } } void @@ -1302,6 +1430,34 @@ class Delegate_test : public beast::unit_test::Suite env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfFullyCanonicalSig), delegate::As(bob)); } + + { + Env env(*this); + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob)); + env.close(); + + // sfQualityOut is a valid TrustSet field, but not permitted in granular template + json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth); + txJson[sfQualityOut.jsonName] = 100; + env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // tfSetNoRipple is a valid flag for TrustSet, but not permitted in granular template + env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfSetNoRipple), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } } void @@ -1457,7 +1613,9 @@ class Delegate_test : public beast::unit_test::Suite env(jv2, Ter(terNO_DELEGATE_PERMISSION)); } - // can not set AccountSet flags on behalf of other account + // can not set AccountSet flags on behalf of other account, + // in permissions.macro, the template for AccountSet does + // not allow any flag set or clear. { Env env(*this); auto const alice = Account{"alice"}; @@ -1553,6 +1711,71 @@ class Delegate_test : public beast::unit_test::Suite env(jt); BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); } + + // setting invalid field not in permissions.macro template will be rejected. + { + Env env(*this); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // Alice gives Bob permission to set her Domain + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + std::string const domain = "example.com"; + auto txJson = noop(alice); + txJson[sfDomain] = strHex(domain); + txJson[sfDelegate] = bob.human(); + + // sfNFTokenMinter is a valid field in AccountSet tx, but + // it is not permitted for granular template + txJson[sfNFTokenMinter] = bob.human(); + + env(txJson, Ter(terNO_DELEGATE_PERMISSION)); + } + + // Delegated AccountSet with no fields and no flags is allowed, + // because it is allowed in the non-delegated case as well. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + auto jt = noop(alice); + jt[sfDelegate] = bob.human(); + env(jt); + } + + // Revoking all permissions deletes the SLE and subsequent attempts are rejected. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + env(jt); + + // empty DelegateSet deletes the SLE + env(delegate::set(alice, bob, {})); + env.close(); + + env(jt, Ter(terNO_DELEGATE_PERMISSION)); + } } void @@ -1673,6 +1896,37 @@ class Delegate_test : public beast::unit_test::Suite env.close(); mpt.set({.account = alice, .flags = tfMPTLock | tfFullyCanonicalSig, .delegate = bob}); } + + // field not permitted to exist in granular delegation + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(100000), alice, bob); + + MPTTester mpt(env, alice, {.fund = false}); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob for MPTokenIssuanceLock + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + + // Field is not permitted, permitted fields for delegation is defined in + // permissions.macro. + mpt.set( + {.account = alice, + .mutableFlags = 2, + .delegate = bob, + .err = terNO_DELEGATE_PERMISSION}); + + // Notice: flags not defined in permissions.macro are not permitted for delegation. + // Since preflight will check invalid flag for the tx, it is not reachable. + // If any new flag is defined into the transaction in the future, + // but is not allowed for delegation, the transaction will be rejected with + // terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in + // permissions.macro. + } } void @@ -2145,6 +2399,62 @@ class Delegate_test : public beast::unit_test::Suite for (auto const& tx : txRequiredFeatures) txAmendmentEnabled(tx.first); } + + // Granular permissions also require the amendment for their underlying + // transaction type. + { + for (auto const permission : {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}) + { + Env env(*this, features - featureMPTokensV1); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {permission}), Ter(temMALFORMED)); + } + } + } + + void + testGranularSandboxCheckOrder() + { + testcase("Make sure GranularSandbox is checked after transaction-level permission"); + + using namespace jtx; + + Env env(*this); + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob)); + env.close(); + + // sfQualityOut is a valid TrustSet field, but not permitted in granular template + json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth); + txJson[sfQualityOut.jsonName] = 100; + env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // Now Alice grants Bob with transaction level permission + env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustSet"})); + env.close(); + + // NOTE: This case is to ensure that if a delegate possesses a + // transaction-level permission (e.g., TrustSet), the granular sandbox must not incorrectly + // block the transaction. The function checkGranularSandbox MUST be called after the + // transaction-level permission check. This test case is to avoid future refactor mistakes, + // modifying the order will fail here. + env(txJson, delegate::As(bob)); } void @@ -2197,6 +2507,94 @@ class Delegate_test : public beast::unit_test::Suite "\n Action: Verify security requirements to interact with Delegation feature"); } + void + testNonDelegableTxWithDelegate(FeatureBitset features) + { + testcase("non-delegable tx with sfDelegate is rejected at preflight"); + using namespace jtx; + + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // Transactions that are notDelegable and have no granular permissions + // will be rejected with temINVALID at preflight. + // Note: pseudo-transactions (EnableAmendment, SetFee and UNLModify) are also + // notDelegable but are excluded here — passesLocalChecks() blocks them + // before preflight1 is ever reached. + { + // SetRegularKey, SignerListSet, AccountDelete, DelegateSet. + env(regkey(alice, bob), delegate::As(bob), Ter(temINVALID)); + env(signers(alice, 1, {{bob, 1}}), delegate::As(bob), Ter(temINVALID)); + env(acctdelete(alice, bob), delegate::As(bob), Ter(temINVALID)); + env(delegate::set(alice, bob, {"Payment"}), delegate::As(bob), Ter(temINVALID)); + + // SAV transactions. + { + Vault const vault{env}; + auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(createTx, delegate::As(bob), Ter(temINVALID)); + + env(vault.set({.owner = alice, .id = keylet.key}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.del({.owner = alice, .id = keylet.key}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(1)}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(1)}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.clawback({.issuer = alice, .id = keylet.key, .holder = bob}), + delegate::As(bob), + Ter(temINVALID)); + } + + // Batch transaction: the outer Batch itself is non-delegable. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 1); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(1)), seq + 1), + delegate::As(bob), + Ter(temINVALID)); + } + + // Lending protocol transactions + { + Vault const vault{env}; + auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(createTx); + + env(loanBroker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loanBroker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loanBroker::coverDeposit(alice, keylet.key, XRP(1)), + delegate::As(bob), + Ter(temINVALID)); + env(loanBroker::coverWithdraw(alice, keylet.key, XRP(1)), + delegate::As(bob), + Ter(temINVALID)); + env(loanBroker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); + + env(loan::set(alice, keylet.key, Number(100)), delegate::As(bob), Ter(temINVALID)); + env(loan::manage(alice, keylet.key, 0), delegate::As(bob), Ter(temINVALID)); + env(loan::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan::pay(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); + } + } + + // AccountSet is notDelegable at tx level but has granular permissions, + // so sfDelegate passes preflight and is rejected at invokeCheckPermission with + // terNO_DELEGATE_PERMISSION. + { + env(fset(alice, asfDefaultRipple), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + } + void testDelegateUtilsNullptrCheck() { @@ -2206,9 +2604,8 @@ class Delegate_test : public beast::unit_test::Suite STTx const tx{ttPAYMENT, [](STObject&) {}}; BEAST_EXPECT(checkTxPermission(nullptr, tx) == terNO_DELEGATE_PERMISSION); - // loadGranularPermission nullptr check - std::unordered_set granularPermissions; - loadGranularPermission(nullptr, ttPAYMENT, granularPermissions); + // getGranularPermission nullptr check + auto const granularPermissions = getGranularPermission(nullptr, ttPAYMENT); BEAST_EXPECT(granularPermissions.empty()); } @@ -2238,7 +2635,9 @@ class Delegate_test : public beast::unit_test::Suite testSignForDelegated(); testPermissionValue(all); testTxRequireFeatures(all); + testGranularSandboxCheckOrder(); testTxDelegableCount(); + testNonDelegableTxWithDelegate(all); testDelegateUtilsNullptrCheck(); } }; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index eef6af1fea..84a4dfddac 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include @@ -1342,6 +1343,87 @@ class Invariants_test : public beast::unit_test::Suite }); } + void + testAMMDeleteInvariants(FeatureBitset features) + { + using namespace test::jtx; + + bool const enforceAMMDelete = features[fixCleanup3_3_0]; + testcase << "AMM delete invariants" + std::string(enforceAMMDelete ? " fix" : ""); + + Env env(*this, features); + Account const issuer{"issuer"}; + Issue const lptIssue{Currency(0x4c50540000000000), issuer.id()}; + STAmount const zeroLP{lptIssue, 0}; + STAmount const nonZeroLP{lptIssue, 1}; + + auto const makeAMM = [](STAmount const& lptBalance) { + auto sleAMM = std::make_shared(keylet::amm(uint256(1))); + sleAMM->setFieldAmount(sfLPTokenBalance, lptBalance); + return sleAMM; + }; + + auto const checkInvariant = [&](TxType txType, + TER result, + std::optional const& deletedLPBalance, + bool expected, + std::string const& expectedLog) { + test::StreamSink sink{beast::Severity::Warning}; + beast::Journal const jlog{sink}; + ValidAMM invariant; + + if (deletedLPBalance) + invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr); + + bool const actual = invariant.finalize( + STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog); + + BEAST_EXPECTS(actual == expected, "unexpected AMM delete invariant result"); + auto const messages = sink.messages().str(); + auto const expectedLogWhenEnforced = enforceAMMDelete ? expectedLog : ""; + if (!expectedLogWhenEnforced.empty()) + { + BEAST_EXPECTS(messages.contains(expectedLogWhenEnforced), expectedLogWhenEnforced); + } + else + { + BEAST_EXPECTS(messages.empty(), messages); + } + }; + + checkInvariant( + ttPAYMENT, + tesSUCCESS, + nonZeroLP, + !enforceAMMDelete, + "Invariant failed: AMM failed, unexpected AMM deletion by"); + checkInvariant( + ttAMM_DELETE, + tesSUCCESS, + std::nullopt, + !enforceAMMDelete, + "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS"); + checkInvariant( + ttAMM_DELETE, + tesSUCCESS, + nonZeroLP, + !enforceAMMDelete, + "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP balance"); + checkInvariant( + ttAMM_DELETE, + tecINCOMPLETE, + zeroLP, + !enforceAMMDelete, + "Invariant failed: AMMDelete failed, AMM object deleted when result is not tesSUCCESS"); + + checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, nonZeroLP, true, ""); + checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, nonZeroLP, true, ""); + + checkInvariant(ttAMM_DELETE, tesSUCCESS, zeroLP, true, ""); + checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, zeroLP, true, ""); + checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, zeroLP, true, ""); + } + static SLE::pointer createPermissionedDomain( ApplyContext& ac, @@ -5063,6 +5145,8 @@ public: testNoZeroEscrow(); testValidNewAccountRoot(); testNFTokenPageInvariants(); + testAMMDeleteInvariants(defaultAmendments()); + testAMMDeleteInvariants(defaultAmendments() - fixCleanup3_3_0); testPermissionedDomainInvariants(defaultAmendments() | fixCleanup3_1_3); testPermissionedDomainInvariants(defaultAmendments() - fixCleanup3_1_3); testPermissionedDEX(defaultAmendments() | fixCleanup3_1_3); @@ -5077,6 +5161,7 @@ public: testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3); testVaultComputeCoarsestScale(); testAMM(); + testSponsorship(); } }; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 7cc3a8ac48..423fe7b6cc 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -19,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -4437,11 +4437,12 @@ protected: Account const lender{"lender"}; Account const issuer{"issuer"}; Account const borrower{"borrower"}; + Account const sponsor{"sponsor"}; auto const iou = issuer["IOU"]; auto testWrapper = [&](auto&& test) { Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); + env.fund(XRP(1'000), lender, issuer, borrower, sponsor); env(trust(lender, iou(10'000'000))); env(pay(issuer, lender, iou(5'000'000))); BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; @@ -4456,6 +4457,15 @@ protected: BrokerInfo const& brokerInfo, jtx::Fee const& loanSetFee, Number const& debtMaximumRequest) { + for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) + { + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sponsor::As(sponsor, sponsorFlags), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + } + // first temBAD_SIGNER: TODO // invalid grace period { @@ -5423,110 +5433,12 @@ protected: } void - testCoverDepositWithdrawNonTransferableMPT(FeatureBitset feature) + testLendingCanTradeDisabledNoImpact() { - testcase("CoverDeposit blocked, CoverWithdraw allowed when CanTransfer cleared"); - using namespace jtx; - using namespace loanBroker; - - Env env(*this, feature); - - Account const issuer{"issuer"}; - Account const alice{"alice"}; - - env.fund(XRP(100'000), issuer, alice); - env.close(); - - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {alice}, - .pay = 100, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mpt["MPT"]; - - Vault const vault{env}; - auto const [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); - env(createTx); - env.close(); - - auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, vaultKeylet.key)); - env.close(); - - auto const brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - - Account const pseudoAccount{"Loan Broker pseudo-account", brokerSle->at(sfAccount)}; - - // First, deposit some cover while CanTransfer is set so we have an - // existing position to withdraw from after the governance action. - auto const depositAmount = asset(1); - env(coverDeposit(alice, brokerKeylet.key, depositAmount)); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(Balance(pseudoAccount, depositAmount)); - } - - // Issuer governance: clear CanTransfer. - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Standard Payment path still forbids third-party transfers. - auto const err = feature[featureMPTokensV2] ? tecNO_PERMISSION : tecNO_AUTH; - env(pay(alice, pseudoAccount, asset(1)), Ter(err)); - env.close(); - - // New cover deposits are blocked - this would create new exposure. - env(coverDeposit(alice, brokerKeylet.key, depositAmount), Ter{tecNO_AUTH}); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(Balance(pseudoAccount, depositAmount)); - } - - bool const postAmendment = feature[fixCleanup3_2_0]; - if (postAmendment) - { - // Post-fixCleanup3_2_0: existing cover can always be withdrawn - // even when CanTransfer is cleared, so the broker is not trapped. - env(coverWithdraw(alice, brokerKeylet.key, depositAmount)); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); - env.require(Balance(pseudoAccount, asset(0))); - } - } - else - { - // Pre-fixCleanup3_2_0 regression: cover withdraw was blocked, - // trapping the broker's first-loss capital. - env(coverWithdraw(alice, brokerKeylet.key, depositAmount), Ter{tecNO_AUTH}); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(Balance(pseudoAccount, depositAmount)); - } - } - } - - void - testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared() - { - testcase("LoanSet blocked, LoanPay allowed when CanTransfer cleared"); + testcase("Lending: CanTrade disabled has no impact"); using namespace jtx; using namespace loan; + using namespace loanBroker; Env env(*this, all_); @@ -5542,67 +5454,7 @@ protected: .issuer = issuer, .holders = {lender, borrower}, .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mpt.issuanceID(); - env(pay(issuer, lender, asset(10'000'000))); - // Fund the borrower with enough to cover principal+interest+fees - env(pay(issuer, borrower, asset(100'000))); - env.close(); - - // Create vault and broker while CanTransfer is set. - auto const broker = createVaultAndBroker(env, asset, lender); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - // Create an existing loan while CanTransfer is set. - env(set(borrower, broker.brokerID, 1'000), - Sig(sfCounterpartySignature, lender), - loanSetFee); - env.close(); - auto const loanKeylet = keylet::loan(broker.brokerID, 1); - BEAST_EXPECT(env.le(loanKeylet)); - - // Issuer governance: clear CanTransfer. - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Issuing a NEW loan is blocked - it would create new exposure into - // a pool the issuer is restricting. - env(set(borrower, broker.brokerID, 1'000), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter{tecNO_AUTH}); - env.close(); - - // Repaying an existing loan is always allowed - blocking it would - // create irrecoverable bad debt and trap SAV depositor principal. - env(pay(borrower, loanKeylet.key, asset(1'000))); - env.close(); - } - - void - testLendingCanTradeClearedNoImpact() - { - testcase("Lending: CanTrade cleared has no impact"); - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); PrettyAsset const asset = mpt.issuanceID(); env(pay(issuer, lender, asset(10'000'000))); env(pay(issuer, borrower, asset(100'000))); @@ -5610,16 +5462,7 @@ protected: auto const broker = createVaultAndBroker(env, asset, lender); - // Sanity: while CanTrade is set, the asset can be placed on the DEX. - env(offer(lender, XRP(1), asset(10))); - env.close(); - - // Issuer governance: clear CanTrade. Loan origination and repayment - // are not trades: nothing in the Lending Protocol should be impacted. - mpt.set({.mutableFlags = tmfMPTClearCanTrade}); - env.close(); - - // Control: clearing CanTrade is observable on the DEX path. + // CanTrade is not set env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); env.close(); @@ -5644,6 +5487,13 @@ protected: // Cover withdrawal still works. env(coverWithdraw(lender, broker.brokerID, asset(100))); env.close(); + + // Enable CanTrade and verify the DEX path is restored. + mpt.set({.mutableFlags = tmfMPTSetCanTrade}); + env.close(); + + env(offer(lender, XRP(1), asset(10))); + env.close(); } #if LOAN_TODO @@ -8716,8 +8566,7 @@ protected: testRIPD3901(); testBorrowerIsBroker(); testLimitExceeded(); - testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared(); - testLendingCanTradeClearedNoImpact(); + testLendingCanTradeDisabledNoImpact(); testBugOverpaymentPrincipalChange(); testBugOverpayUnroundedAmount(); @@ -8747,7 +8596,6 @@ protected: testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features); testBatchBypassCounterparty(features); testLoanNextPaymentDueDateOverflow(features); - testCoverDepositWithdrawNonTransferableMPT(features); testSequentialFLCDepletion(features); // Invariants diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index dedc701156..b6c6ef5d93 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -3494,7 +3494,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanMutateCanLock | + .mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanEnableCanLock | tmfMPTCanMutateTransferFee}); // Setting flags is not allowed when MutableFlags is present @@ -3542,33 +3542,6 @@ class MPToken_test : public beast::unit_test::Suite } } - // Can not set and clear the same mutable flag - { - Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {bob}}); - auto const mptID = makeMptID(env.seq(alice), alice); - - auto const flagCombinations = { - tmfMPTSetCanLock | tmfMPTClearCanLock, - tmfMPTSetRequireAuth | tmfMPTClearRequireAuth, - tmfMPTSetCanEscrow | tmfMPTClearCanEscrow, - tmfMPTSetCanTrade | tmfMPTClearCanTrade, - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer, - tmfMPTSetCanClawback | tmfMPTClearCanClawback, - tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade, - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanEscrow | - tmfMPTClearCanClawback}; - - for (auto const& mutableFlags : flagCombinations) - { - mptAlice.set( - {.account = alice, - .id = mptID, - .mutableFlags = mutableFlags, - .err = temINVALID_FLAG}); - } - } - // Can not mutate flag which is not mutable { Env env{*this, features}; @@ -3578,17 +3551,11 @@ class MPToken_test : public beast::unit_test::Suite auto const mutableFlags = { tmfMPTSetCanLock, - tmfMPTClearCanLock, tmfMPTSetRequireAuth, - tmfMPTClearRequireAuth, tmfMPTSetCanEscrow, - tmfMPTClearCanEscrow, tmfMPTSetCanTrade, - tmfMPTClearCanTrade, tmfMPTSetCanTransfer, - tmfMPTClearCanTransfer, - tmfMPTSetCanClawback, - tmfMPTClearCanClawback}; + tmfMPTSetCanClawback}; for (auto const& mutableFlag : mutableFlags) { @@ -3632,34 +3599,6 @@ class MPToken_test : public beast::unit_test::Suite .err = temBAD_TRANSFER_FEE}); } - // Test setting non-zero transfer fee and clearing MPTCanTransfer at the - // same time - { - Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {bob}}); - - mptAlice.create( - {.transferFee = 100, - .ownerCount = 1, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); - - // Can not set non-zero transfer fee and clear MPTCanTransfer at the - // same time - mptAlice.set( - {.account = alice, - .mutableFlags = tmfMPTClearCanTransfer, - .transferFee = 1, - .err = temMALFORMED}); - - // Can set transfer fee to zero and clear MPTCanTransfer at the same - // time. tfMPTCanTransfer will be cleared and TransferFee field will - // be removed. - mptAlice.set( - {.account = alice, .mutableFlags = tmfMPTClearCanTransfer, .transferFee = 0}); - BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - } - // Can not set non-zero transfer fee when MPTCanTransfer is not set { Env env{*this, features}; @@ -3667,7 +3606,7 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanEnableCanTransfer}); mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); @@ -3700,21 +3639,14 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | + .mutableFlags = tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanMutateMetadata}); // Can not mutate transfer fee mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); auto const invalidFlags = { - tmfMPTSetCanLock, - tmfMPTClearCanLock, - tmfMPTSetRequireAuth, - tmfMPTClearRequireAuth, - tmfMPTSetCanEscrow, - tmfMPTClearCanEscrow, - tmfMPTSetCanClawback, - tmfMPTClearCanClawback}; + tmfMPTSetCanLock, tmfMPTSetRequireAuth, tmfMPTSetCanEscrow, tmfMPTSetCanClawback}; // Can not mutate flags which are not mutable for (auto const& mutableFlag : invalidFlags) @@ -3725,11 +3657,9 @@ class MPToken_test : public beast::unit_test::Suite // Can mutate MPTCanTrade mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade}); // Can mutate MPTCanTransfer mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); // Can mutate metadata mptAlice.set({.account = alice, .metadata = "test"}); @@ -3798,37 +3728,26 @@ class MPToken_test : public beast::unit_test::Suite BEAST_EXPECT(mptAlice.checkTransferFee(10)); } - // Test flag toggling + // Test mutable flag enablement { - auto testFlagToggle = [&](std::uint32_t createFlags, - std::uint32_t setFlags, - std::uint32_t clearFlags) { + auto testFlagSet = [&](std::uint32_t createFlags, std::uint32_t setFlags) { Env env{*this, features}; MPTTester mptAlice(env, alice); // Create the MPT object with the specified initial flags mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = createFlags}); - // Set and clear the flag multiple times - mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); + // Setting the same mutable capability more than once is harmless. mptAlice.set({.account = alice, .mutableFlags = setFlags}); mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); - mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = clearFlags}); }; - testFlagToggle(tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock); - testFlagToggle( - tmfMPTCanMutateRequireAuth, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth); - testFlagToggle(tmfMPTCanMutateCanEscrow, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow); - testFlagToggle(tmfMPTCanMutateCanTrade, tmfMPTSetCanTrade, tmfMPTClearCanTrade); - testFlagToggle( - tmfMPTCanMutateCanTransfer, tmfMPTSetCanTransfer, tmfMPTClearCanTransfer); - testFlagToggle( - tmfMPTCanMutateCanClawback, tmfMPTSetCanClawback, tmfMPTClearCanClawback); + testFlagSet(tmfMPTCanEnableCanLock, tmfMPTSetCanLock); + testFlagSet(tmfMPTCanEnableRequireAuth, tmfMPTSetRequireAuth); + testFlagSet(tmfMPTCanEnableCanEscrow, tmfMPTSetCanEscrow); + testFlagSet(tmfMPTCanEnableCanTrade, tmfMPTSetCanTrade); + testFlagSet(tmfMPTCanEnableCanTransfer, tmfMPTSetCanTransfer); + testFlagSet(tmfMPTCanEnableCanClawback, tmfMPTSetCanClawback); } } @@ -3849,7 +3768,7 @@ class MPToken_test : public beast::unit_test::Suite {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanTrade | + .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanTrade | tmfMPTCanMutateTransferFee}); mptAlice.authorize({.account = bob, .holderCount = 1}); @@ -3857,11 +3776,8 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); // Can mutate the mutable flags and fields - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade}); mptAlice.set({.account = alice, .transferFee = 200}); } @@ -3873,7 +3789,7 @@ class MPToken_test : public beast::unit_test::Suite {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | + .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanClawback | tmfMPTCanMutateMetadata}); mptAlice.authorize({.account = bob, .holderCount = 1}); @@ -3881,36 +3797,23 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set({.account = alice, .flags = tfMPTLock}); // Can mutate the mutable flags and fields - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback}); mptAlice.set({.account = alice, .metadata = "mutate"}); } - // Test lock and unlock after mutating MPTCanLock + // Test lock and unlock after enabling MPTCanLock { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, .holderCount = 0, - .flags = tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | + .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanClawback | tmfMPTCanMutateMetadata}); mptAlice.authorize({.account = bob, .holderCount = 1}); - // Can lock and unlock - mptAlice.set({.account = alice, .flags = tfMPTLock}); - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); - mptAlice.set({.account = alice, .flags = tfMPTUnlock}); - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); - - // Clear lsfMPTCanLock - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock}); - - // Can not lock or unlock + // Can not lock or unlock before MPTCanLock is enabled mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION}); mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); mptAlice.set( @@ -3918,10 +3821,10 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set( {.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); - // Set MPTCanLock again + // Set MPTCanLock mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - // Can lock and unlock again + // Can lock and unlock mptAlice.set({.account = alice, .flags = tfMPTLock}); mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); mptAlice.set({.account = alice, .flags = tfMPTUnlock}); @@ -3935,83 +3838,30 @@ class MPToken_test : public beast::unit_test::Suite testcase("Mutate MPTRequireAuth"); using namespace test::jtx; - // test mutating RequireAuth flag on the issuance and its effect on payment authorization - { - Env env{*this, features}; - Account const alice("alice"); - Account const bob("bob"); + // test enabling RequireAuth flag on the issuance and its effect on payment + // authorization + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); - MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .flags = tfMPTRequireAuth, - .mutableFlags = tmfMPTCanMutateRequireAuth}); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = tmfMPTCanEnableRequireAuth}); - mptAlice.authorize({.account = bob}); - mptAlice.authorize({.account = alice, .holder = bob}); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 1000); - // Pay to bob - mptAlice.pay(alice, bob, 1000); + // Set RequireAuth because it is mutable. + mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); - // Unauthorize bob - mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); + // This should fail because bob is not authorized yet. + mptAlice.pay(alice, bob, 1000, tecNO_AUTH); - // Can not pay to bob - mptAlice.pay(bob, alice, 100, tecNO_AUTH); - - // Clear RequireAuth - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth}); - - // Can pay to bob - mptAlice.pay(alice, bob, 1000); - - // Set RequireAuth again - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); - - // Can not pay to bob since he is not authorized - mptAlice.pay(bob, alice, 100, tecNO_AUTH); - - // Authorize bob again - mptAlice.authorize({.account = alice, .holder = bob}); - - // Can pay to bob again - mptAlice.pay(alice, bob, 100); - } - - // Cannot clear RequireAuth when a DomainID is set on the issuance - { - Account const alice{"alice"}; - Account const bob{"bob"}; - Account const credIssuer{"credIssuer"}; - pdomain::Credentials const credentials{ - {.issuer = credIssuer, .credType = "credential"}}; - - Env env{*this, features}; - env.fund(XRP(1000), credIssuer); - env.close(); - - env(pdomain::setTx(credIssuer, credentials)); - env.close(); - auto const domainId = pdomain::getNewDomain(env.meta()); - - MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create({ - .ownerCount = 1, - .flags = tfMPTRequireAuth, - .mutableFlags = tmfMPTCanMutateRequireAuth, - .domainID = domainId, - }); - - // Clearing RequireAuth while a DomainID is present must be rejected, - mptAlice.set({ - .account = alice, - .mutableFlags = tmfMPTClearRequireAuth, - .err = tecNO_PERMISSION, - }); - - // Setting RequireAuth (already set) is still allowed, though it has no effect. - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); - } + // Issuer authorizes bob and pay should succeed. + mptAlice.authorize({.account = alice, .holder = bob}); + mptAlice.pay(alice, bob, 1000); } void @@ -4032,7 +3882,7 @@ class MPToken_test : public beast::unit_test::Suite {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanEscrow}); + .mutableFlags = tmfMPTCanEnableCanEscrow}); mptAlice.authorize({.account = carol}); mptAlice.authorize({.account = bob}); @@ -4054,14 +3904,6 @@ class MPToken_test : public beast::unit_test::Suite escrow::kCondition(escrow::kCb1), escrow::kFinishTime(env.now() + 1s), Fee(baseFee * 150)); - - // Clear MPTCanEscrow - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanEscrow}); - env(escrow::create(carol, bob, mpt(3)), - escrow::kCondition(escrow::kCb1), - escrow::kFinishTime(env.now() + 1s), - Fee(baseFee * 150), - Ter(tecNO_PERMISSION)); } void @@ -4080,7 +3922,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee}); + .mutableFlags = tmfMPTCanEnableCanTransfer | tmfMPTCanMutateTransferFee}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); @@ -4124,19 +3966,9 @@ class MPToken_test : public beast::unit_test::Suite env(pay(bob, carol, mptAlice(50)), Txflags(tfPartialPayment)); BEAST_EXPECT(env.balance(carol, mptc) == mptc(49)); } - - // Alice clears MPTCanTransfer - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); - - // TransferFee field is removed when MPTCanTransfer is cleared - BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - - // Bob can not pay - mptAlice.pay(bob, carol, 50, tecNO_AUTH); } - // Can set transfer fee to zero when MPTCanTransfer is not set, but - // tmfMPTCanMutateTransferFee is set. + // Can set transfer fee to zero when tmfMPTCanMutateTransferFee is set. { Env env{*this, features}; @@ -4145,18 +3977,12 @@ class MPToken_test : public beast::unit_test::Suite {.transferFee = 100, .ownerCount = 1, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanMutateTransferFee}); BEAST_EXPECT(mptAlice.checkTransferFee(100)); - // Clear MPTCanTransfer and transfer fee is removed - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer}); - BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - - // Can still set transfer fee to zero, although it is already zero + // Setting transfer fee to zero removes the field. mptAlice.set({.account = alice, .transferFee = 0}); - - // TransferFee field is still not present BEAST_EXPECT(!mptAlice.isTransferFeePresent()); } } @@ -4174,7 +4000,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( - {.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanMutateCanClawback}); + {.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanEnableCanClawback}); // Bob creates an MPToken mptAlice.authorize({.account = bob}); @@ -4190,12 +4016,6 @@ class MPToken_test : public beast::unit_test::Suite // Can clawback now mptAlice.claw(alice, bob, 1); - - // Clear MPTCanClawback - mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback}); - - // Can not clawback - mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); } void @@ -4545,27 +4365,22 @@ class MPToken_test : public beast::unit_test::Suite { Env env(*this); env.fund(XRP(1'000), gw, alice, carol); - MPTTester btc( + MPTTester const btc( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, - .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester eth( + .flags = tfMPTCanTrade | tfMPTCanTransfer}); + MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, - .flags = tfMPTCanTrade | tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .flags = tfMPTCanTrade}); // Can create env(offer(alice, eth(10), btc(10)), Txflags(tfPassive)); - btc.set({.mutableFlags = tmfMPTSetCanTransfer}); - eth.set({.mutableFlags = tmfMPTClearCanTransfer}); - env(offer(alice, eth(10), btc(10)), Txflags(tfPassive)); - BEAST_EXPECT(getAccountOffers(env, alice)[jss::offers].size() == 2); + BEAST_EXPECT(getAccountOffers(env, alice)[jss::offers].size() == 1); // issuer can create env(offer(gw, eth(10), btc(10)), Txflags(tfPassive)); @@ -4593,14 +4408,14 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol}, .pay = 100, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); // Can't create env(offer(gw, eth(10), btc(10)), Ter(tecNO_PERMISSION)); @@ -4837,29 +4652,29 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol, bob}, .pay = 1'000, .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanTrade | - tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanTrade | + tmfMPTCanEnableCanTransfer}); MPTTester eth( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); MPTTester const usd( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); MPTTester const cad( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive)); env.close(); @@ -4905,13 +4720,33 @@ class MPToken_test : public beast::unit_test::Suite // BTC is transferred from ed to bob, ed is not authorized env(pay(ed, gw, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecNO_AUTH)); env.close(); - btc.set({.mutableFlags = tmfMPTClearRequireAuth}); + } - // MPTCanTransfer is not set + // MPTCanTransfer is not set. + { + auto const ed = Account{"ed"}; + Env env{*this, features}; + env.fund(XRP(1'000), gw, alice, carol, bob, ed); + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob, ed}, + .pay = 1'000, + .flags = tfMPTCanTrade}); + MPTTester const eth( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob, ed}, + .pay = 1'000, + .flags = kMptDexFlags}); + + env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive)); + env.close(); + env(offer(bob, btc(1'000), eth(1'000)), Txflags(tfPassive)); + env.close(); // Fail regardless if source/destination is the issuer or // not since the offer is owned by a holder. - btc.set({.mutableFlags = tmfMPTClearCanTransfer}); env(pay(ed, carol, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL)); env(pay(carol, ed, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL)); env(pay(ed, carol, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecPATH_PARTIAL)); @@ -4935,124 +4770,166 @@ class MPToken_test : public beast::unit_test::Suite env(pay(ed, gw, btc(10)), Path(~btc), Sendmax(eth(10))); env.close(); } - // Multiple steps: CAD/USD, USD/BTC, BTC/ETH + + // Multiple steps: CAD/USD, USD/BTC, BTC/ETH. + // takerGets can transfer if: + // - CanTransfer is set + // - The offer's owner is the issuer + // - BookStep is the last step, which means strand's destination is + // the issuer + // takerPays can transfer if + // - BookStep is the first step, which means strand's source is + // the issuer + // - The offer's owner is the issuer + // - Previous step is BookStep, which transfers per above + // - CanTransfer is set { - auto const ed = Account{"ed"}; - Env env{*this, features}; - env.fund(XRP(1'000), gw, alice, carol, bob, ed); - env.close(); - MPTTester btc( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester eth( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester usd( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - MPTTester cad( - {.env = env, - .issuer = gw, - .holders = {alice, carol, bob}, - .pay = 1'000, - .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - // takerGets can transfer if: - // - CanTransfer is set - // - The offer's owner is the issuer - // - BookStep is the last step, which means strand's destination is - // the issuer - // takerPays can transfer if - // - BookStep is the first step, which means strand's source is - // the issuer - // - The offer's owner is the issuer - // - Previous step is BookStep, which transfers per above - // - CanTransfer is set - env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); - env(offer(bob, usd(100), btc(100)), Txflags(tfPassive)); - env(offer(bob, btc(100), eth(100)), Txflags(tfPassive)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - btc.set({.mutableFlags = tmfMPTSetCanTransfer}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // TakerGets - // fail - CAD/USD is owned by bob - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - auto seq(env.seq(gw)); - env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); - env.close(); - // fail - CAD/USD is owned by bob - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - env.close(); - env(offerCancel(gw, seq)); - env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - // succeed - CAD/USD is owned by issuer - env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - // bob's CAD/USD is deleted - BEAST_EXPECT(expectOffers(env, bob, 2)); - env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); - BEAST_EXPECT(expectOffers(env, gw, 0)); - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - eth.set({.mutableFlags = tmfMPTClearCanTransfer}); - // fail - BTC/ETH is owned by bob, destination is carol - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - // succeed - destination is an issuer - env(pay(alice, gw, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - // TakerPays - eth.set({.mutableFlags = tmfMPTSetCanTransfer}); - cad.set({.mutableFlags = tmfMPTClearCanTransfer}); - // fail - CAD/USD is owned by bob, source is alice - env(pay(alice, carol, eth(1)), - Path(~usd, ~btc, ~eth), - Sendmax(cad(1)), - Ter(tecPATH_PARTIAL)); - // succeed - source is the issuer - env(pay(gw, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); - env.close(); - // succeed - CAD/USD is owned by issuer - env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - BEAST_EXPECT(expectOffers(env, gw, 0)); - BEAST_EXPECT(expectOffers(env, bob, 2)); - cad.set({.mutableFlags = tmfMPTSetCanTransfer}); - btc.set({.mutableFlags = tmfMPTClearCanTransfer}); - env(offer(bob, cad(1), usd(1)), Txflags(tfPassive)); - env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); - env.close(); - // succeed - USD/BTC is owned by issuer - env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); - env.close(); - BEAST_EXPECT(expectOffers(env, gw, 0)); + // enum to indicate which MPT doesn't set CanTransfer flag. + enum class NoTransferMPT { BTC, ETH, USD, CAD }; + + // Lambda to test multi-step payment with one of the MPTs not setting CanTransfer flag. + auto const testMultiStepMPTCanTransfer = [&](NoTransferMPT const noTransferMPT, + auto const& test) { + auto const getFlags = [&](NoTransferMPT const mpt) { + return mpt == noTransferMPT ? tfMPTCanTrade : kMptDexFlags; + }; + + Env env{*this, features}; + env.fund(XRP(1'000), gw, alice, carol, bob); + env.close(); + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::BTC)}); + MPTTester const eth( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::ETH)}); + MPTTester const usd( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::USD)}); + MPTTester const cad( + {.env = env, + .issuer = gw, + .holders = {alice, carol, bob}, + .pay = 1'000, + .flags = getFlags(NoTransferMPT::CAD)}); + + env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); + env(offer(bob, usd(100), btc(100)), Txflags(tfPassive)); + env(offer(bob, btc(100), eth(100)), Txflags(tfPassive)); + env.close(); + + test(env, btc, eth, usd, cad); + }; + + // USD starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::USD, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + BEAST_EXPECT(expectOffers(env, bob, 3)); + + // fail - CAD/USD is owned by bob + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + + auto seq(env.seq(gw)); + env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); + env.close(); + // fail - CAD/USD is owned by bob + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + env.close(); + env(offerCancel(gw, seq)); + env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 3)); + // succeed - CAD/USD is owned by issuer + env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + // bob's CAD/USD is deleted. + BEAST_EXPECT(expectOffers(env, bob, 2)); + env(offer(bob, cad(100), usd(100)), Txflags(tfPassive)); + BEAST_EXPECT(expectOffers(env, gw, 0)); + }); + + // ETH starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::ETH, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + // fail - BTC/ETH is owned by bob, destination is carol + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 3)); + + // succeed - destination is an issuer + env(pay(alice, gw, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, bob, 3)); + }); + + // CAD starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::CAD, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + // fail - CAD/USD is owned by bob, source is alice + env(pay(alice, carol, eth(1)), + Path(~usd, ~btc, ~eth), + Sendmax(cad(1)), + Ter(tecPATH_PARTIAL)); + // succeed - source is the issuer + env(pay(gw, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + env(offer(gw, cad(1), usd(1)), Txflags(tfPassive)); + env.close(); + // succeed - CAD/USD is owned by issuer + env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, gw, 0)); + BEAST_EXPECT(expectOffers(env, bob, 2)); + }); + + // BTC starts without MPTCanTransfer. + testMultiStepMPTCanTransfer( + NoTransferMPT::BTC, + [&](Env& env, + MPTTester const& btc, + MPTTester const& eth, + MPTTester const& usd, + MPTTester const& cad) { + env(offer(gw, usd(1), btc(1)), Txflags(tfPassive)); + env.close(); + // succeed - USD/BTC is owned by issuer + env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1))); + env.close(); + BEAST_EXPECT(expectOffers(env, gw, 0)); + }); } // MPTCanTrade is not set @@ -5066,48 +4943,38 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol, bob}, .pay = 1'000, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .mutableFlags = tmfMPTCanEnableCanTrade}); MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanTransfer | tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .flags = kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanTransfer | tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTrade}); + .flags = kMptDexFlags}); env(pay(alice, carol, eth(1)), Path(~eth), Sendmax(btc(1)), Ter(tecNO_PERMISSION)); env(pay(alice, carol, btc(1)), Path(~btc), Sendmax(eth(1)), Ter(tecNO_PERMISSION)); env.close(); + // Enable MPTCanTrade so BTC can be crossed through offers. btc.set({.mutableFlags = tmfMPTSetCanTrade}); env(offer(bob, XRP(1), btc(1))); env(offer(bob, btc(1), eth(1))); env(offer(bob, eth(1), usd(1))); env.close(); - btc.set({.mutableFlags = tmfMPTClearCanTrade}); + BEAST_EXPECT(expectOffers(env, bob, 3)); + env(pay(gw, carol, usd(1)), Path(~btc, ~eth, ~usd), Sendmax(XRP(1)), - Txflags(tfPartialPayment | tfNoRippleDirect), - Ter(tecNO_PERMISSION)); + Txflags(tfPartialPayment | tfNoRippleDirect)); env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); - - env(pay(carol, bob, btc(10)), Sendmax(XRP(10)), Ter(tecNO_PERMISSION)); - env(pay(carol, bob, XRP(10)), Sendmax(btc(10)), Ter(tecNO_PERMISSION)); - env(pay(gw, bob, btc(10)), Sendmax(XRP(10)), Ter(tecNO_PERMISSION)); - env(pay(gw, bob, XRP(10)), Sendmax(btc(10)), Ter(tecNO_PERMISSION)); - env(pay(carol, gw, btc(10)), Sendmax(XRP(10)), Ter(tecNO_PERMISSION)); - env(pay(carol, gw, XRP(10)), Sendmax(btc(10)), Ter(tecNO_PERMISSION)); - env.close(); - BEAST_EXPECT(expectOffers(env, bob, 3)); + BEAST_EXPECT(expectOffers(env, bob, 0)); } // Holders are locked @@ -6900,7 +6767,7 @@ class MPToken_test : public beast::unit_test::Suite .issuer = gw, .holders = {alice, carol}, .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); // src is issuer uint256 checkId{keylet::check(gw, env.seq(gw)).key}; @@ -6942,13 +6809,8 @@ class MPToken_test : public beast::unit_test::Suite env.close(); env(pay(gw, alice, mpt(10))); env.close(); - // can't cash - mpt.set({.account = gw, .mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - env(check::cash(carol, checkId, mpt(10)), Ter(tecNO_AUTH)); - env.close(); - // can cash - mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer}); + + // can cash since MPTCanTransfer is enabled env(check::cash(carol, checkId, mpt(10))); env.close(); } @@ -7367,296 +7229,332 @@ class MPToken_test : public beast::unit_test::Suite Env env(*this); env.fund(XRP(1'000'000), gw, alice, carol); - auto usd = MPTTester( - {.env = env, - .issuer = gw, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanTransfer | - tmfMPTCanMutateCanClawback | tmfMPTCanMutateCanTrade}); - auto eur = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000'000}); - auto const increment = env.current()->fees().increment; auto const txfee = Fee(drops(increment)); auto const badMPT = MPT(gw, 1'000); - auto createDeleteAMM = [&](Account const& lp) { - AMM amm( - env, - lp, - usd(1'000), - eur(1'000), - CreateArg{.fee = static_cast(increment.value())}); - amm.withdrawAll(lp); - BEAST_EXPECT(!amm.ammExists()); + auto const makeMPT = [&](std::uint32_t const flags, + Holders holders = {}, + std::uint64_t const pay = 0, + std::optional const mutableFlags = + std::nullopt) { + return MPTTester( + {.env = env, + .issuer = gw, + .holders = holders, + .pay = pay ? std::optional{pay} : std::nullopt, + .flags = flags, + .mutableFlags = mutableFlags}); + }; + + auto const makeDexMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { + return makeMPT( + tfMPTCanLock | kMptDexFlags, + holders, + pay, + tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanTransfer | + tmfMPTCanEnableCanTrade); + }; + + auto const makeNoTransferMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { + return makeMPT( + tfMPTCanLock | tfMPTCanTrade, holders, pay, tmfMPTCanEnableCanTransfer); + }; + + auto const makeNoTradeMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { + return makeMPT( + tfMPTCanLock | tfMPTCanTransfer, holders, pay, tmfMPTCanEnableCanTrade); }; - // // AMMCreate - // - - auto createJv = AMM::createJv(alice, badMPT(1'000), eur(1'000), 0); - - auto createFail = [&](Account const& account, auto const& err) { - createJv[sfAccount] = account.human(); - env(createJv, txfee, Ter(err)); - env.close(); - }; - - // MPTokenIssuance doesn't exist - - createFail(alice, tecOBJECT_NOT_FOUND); - - // MPToken doesn't exist - - createJv[sfAmount] = STAmount{usd(1'000)}.getJson(); - createFail(alice, tecNO_AUTH); - - // alice authorizes MPToken, can create - usd.authorize({.account = alice}); - env(pay(gw, alice, usd(1'000'000)), txfee); - env.close(); - createDeleteAMM(alice); - - // MPTLock is set - - // alice and issuer can't create - usd.set({.flags = tfMPTLock}); - createFail(alice, tecLOCKED); - createFail(gw, tecLOCKED); - - // MPTRequireAuth is set - - // alice is not authorized - usd.set({.flags = tfMPTUnlock}); - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); - createFail(alice, tecNO_AUTH); - // issuer can create - createDeleteAMM(gw); - - // alice is authorized, can create - usd.authorize({.account = gw, .holder = alice}); - createDeleteAMM(alice); - - // MPTCanTransfer is not set - - usd.set({.mutableFlags = tmfMPTClearRequireAuth}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // alice can't create - createFail(alice, tecNO_AUTH); - // issuer can create - createDeleteAMM(gw); - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - // alice can create - createDeleteAMM(alice); - - // MPTCanTrade is not set - - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - usd.set({.mutableFlags = tmfMPTClearCanTrade}); - // alice and issuer can't create - createFail(alice, tecNO_PERMISSION); - createFail(gw, tecNO_PERMISSION); - usd.set({.mutableFlags = tmfMPTSetCanTrade}); - - // - // AMMDeposit - // - - AMM amm(env, gw, usd(1'000), eur(1'000)); - - // MPTokenIssuance doesn't exist - - amm.deposit( - {.account = alice, - .asset1In = badMPT(1), - .asset2In = eur(1), - .assets = std::make_pair(badMPT, eur), - .err = Ter(terNO_AMM)}); - - // MPToken doesn't exist - - amm.deposit( - {.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)}); - - // MPTLock is set - - usd.set({.flags = tfMPTLock}); - // alice and issuer can't deposit - for (auto const& account : {carol, gw}) { + auto usd = makeDexMPT(); + auto eur = makeDexMPT({alice}, 1'000'000); + + auto createDeleteAMM = [&](auto const& asset, Account const& lp) { + AMM amm( + env, + lp, + asset(1'000), + eur(1'000), + CreateArg{.fee = static_cast(increment.value())}); + amm.withdrawAll(lp); + BEAST_EXPECT(!amm.ammExists()); + }; + + auto createFail = [&](auto const& asset, Account const& account, auto const& err) { + auto const createJv = AMM::createJv(account, asset(1'000), eur(1'000), 0); + env(createJv, txfee, Ter(err)); + env.close(); + }; + + // MPTokenIssuance doesn't exist + createFail(badMPT, alice, tecOBJECT_NOT_FOUND); + + // MPToken doesn't exist + createFail(usd, alice, tecNO_AUTH); + + // alice authorizes MPToken, can create + usd.authorize({.account = alice}); + env(pay(gw, alice, usd(1'000'000)), txfee); + env.close(); + createDeleteAMM(usd, alice); + + // MPTLock is set + // alice and issuer can't create + usd.set({.flags = tfMPTLock}); + createFail(usd, alice, tecLOCKED); + createFail(usd, gw, tecLOCKED); + + // MPTRequireAuth is set + // alice is not authorized + usd.set({.flags = tfMPTUnlock}); + usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + createFail(usd, alice, tecNO_AUTH); + // issuer can create + createDeleteAMM(usd, gw); + + // alice is authorized, can create + usd.authorize({.account = gw, .holder = alice}); + createDeleteAMM(usd, alice); + + // MPTCanTransfer is not set + { + auto usd2 = makeNoTransferMPT({alice}, 1'000'000); + + // alice can't create + createFail(usd2, alice, tecNO_AUTH); + // issuer can create + createDeleteAMM(usd2, gw); + usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + // alice can create + createDeleteAMM(usd2, alice); + } + + // MPTCanTrade is not set + { + auto usd3 = makeNoTradeMPT({alice}, 1'000'000); + + // alice and issuer can't create + createFail(usd3, alice, tecNO_PERMISSION); + createFail(usd3, gw, tecNO_PERMISSION); + usd3.set({.mutableFlags = tmfMPTSetCanTrade}); + // alice can create + createDeleteAMM(usd3, alice); + } + } + + // AMMDeposit + { + auto usd = makeDexMPT(); + auto eur = makeDexMPT({alice}, 1'000'000); + AMM amm(env, gw, usd(1'000), eur(1'000)); + + // MPTokenIssuance doesn't exist amm.deposit( - {.account = account, + {.account = alice, + .asset1In = badMPT(1), + .asset2In = eur(1), + .assets = std::make_pair(badMPT, eur), + .err = Ter(terNO_AMM)}); + + // MPToken doesn't exist + amm.deposit( + {.account = carol, .asset1In = usd(1), .asset2In = eur(1), - .err = Ter(tecLOCKED)}); + .err = Ter(tecNO_AUTH)}); + + // Fund carol for the AMMDeposit checks. + usd.authorize({.account = carol}); + env(pay(gw, carol, usd(1'000'000))); + eur.authorize({.account = carol}); + env(pay(gw, carol, eur(1'000'000))); + env.close(); + + // MPTLock is set + usd.set({.flags = tfMPTLock}); + + // alice and issuer can't deposit + for (auto const& account : {carol, gw}) + { + amm.deposit( + {.account = account, + .asset1In = usd(1), + .asset2In = eur(1), + .err = Ter(tecLOCKED)}); + amm.deposit( + {.account = account, + .asset1In = eur(1), + .assets = std::make_pair(eur, usd), + .err = Ter(tecLOCKED)}); + } + usd.set({.flags = tfMPTUnlock}); + + // MPTRequireAuth is set + // carol is not authorized by the issuer + usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + env.close(); amm.deposit( - {.account = account, + {.account = carol, + .asset1In = usd(1), + .asset2In = eur(1), + .err = Ter(tecNO_AUTH)}); + amm.deposit( + {.account = carol, .asset1In = eur(1), .assets = std::make_pair(eur, usd), - .err = Ter(tecLOCKED)}); + .err = Ter(tecNO_AUTH)}); + // issuer can deposit + amm.deposit({.account = gw, .tokens = 1'000}); + // carol is authorized, can deposit + usd.authorize({.account = gw, .holder = carol}); + amm.deposit({.account = carol, .tokens = 1'000}); + // Can't authorize or unauthorize AMM pseudo-account + usd.authorize( + {.account = gw, + .holder = Account{"amm", amm.ammAccount()}, + .err = tecNO_PERMISSION}); + usd.authorize( + {.account = gw, + .holder = Account{"amm", amm.ammAccount()}, + .flags = tfMPTUnauthorize, + .err = tecNO_PERMISSION}); + + // MPTCanTransfer is not set + { + auto usd2 = makeNoTransferMPT({carol}, 1'000'000); + AMM amm2(env, gw, usd2(1'000), eur(1'000)); + + // carol can't deposit + amm2.deposit( + {.account = carol, + .asset1In = usd2(1), + .asset2In = eur(1), + .err = Ter(tecNO_AUTH)}); + amm2.deposit( + {.account = carol, + .asset1In = eur(1), + .assets = std::make_pair(eur, usd2), + .err = Ter(tecNO_AUTH)}); + // issuer can deposit + amm2.deposit({.account = gw, .tokens = 1'000}); + usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + // carol can deposit + amm2.deposit({.account = carol, .tokens = 1'000}); + } } - usd.set({.flags = tfMPTUnlock}); - // MPTRequireAuth is set - - // carol authorizes MPToken but is not authorized by the issuer - usd.authorize({.account = carol}); - env(pay(gw, carol, usd(1'000'000))); - // carol authorizes EUR - eur.authorize({.account = carol}); - env(pay(gw, carol, eur(1'000'000))); - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); - env.close(); - amm.deposit( - {.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)}); - amm.deposit( - {.account = carol, - .asset1In = eur(1), - .assets = std::make_pair(eur, usd), - .err = Ter(tecNO_AUTH)}); - // issuer can deposit - amm.deposit({.account = gw, .tokens = 1'000}); - // carol is authorized, can deposit - usd.authorize({.account = gw, .holder = carol}); - amm.deposit({.account = carol, .tokens = 1'000}); - // Can't authorize or unauthorize AMM pseudo-account - usd.authorize( - {.account = gw, - .holder = Account{"amm", amm.ammAccount()}, - .err = tecNO_PERMISSION}); - usd.authorize( - {.account = gw, - .holder = Account{"amm", amm.ammAccount()}, - .flags = tfMPTUnauthorize, - .err = tecNO_PERMISSION}); - - // MPTCanTransfer is not set - - usd.set({.mutableFlags = tmfMPTClearRequireAuth}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // carol can't deposit - amm.deposit( - {.account = carol, .asset1In = usd(1), .asset2In = eur(1), .err = Ter(tecNO_AUTH)}); - amm.deposit( - {.account = carol, - .asset1In = eur(1), - .assets = std::make_pair(eur, usd), - .err = Ter(tecNO_AUTH)}); - // issuer can deposit - amm.deposit({.account = gw, .tokens = 1'000}); - // carol can deposit - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - amm.deposit({.account = carol, .tokens = 1'000}); - - // MPTCanTrade is not set - - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - usd.set({.mutableFlags = tmfMPTClearCanTrade}); - amm.deposit({.account = gw, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)}); - amm.deposit({.account = carol, .tokens = 1'000, .err = Ter(tecNO_PERMISSION)}); - usd.set({.mutableFlags = tmfMPTSetCanTrade}); - - // // AMMWithdraw - // - - // MPTokenIssuance doesn't exist - - amm.withdraw( - WithdrawArg{ - .account = carol, - .asset1Out = badMPT(1), - .asset2Out = eur(1), - .assets = std::make_pair(badMPT, eur), - .err = Ter(terNO_AMM)}); - - // MPToken doesn't exist - doesn't apply since MPToken is created - // on withdraw in this case - - // MPTLock is set - - usd.set({.flags = tfMPTLock}); - // carol and issuer can't withdraw - for (auto const& account : {carol, gw}) { + auto usd = makeDexMPT(); + auto eur = makeDexMPT({carol}, 1'000'000); + AMM amm(env, gw, usd(1'000), eur(1'000)); + + usd.authorize({.account = carol}); + env(pay(gw, carol, usd(1'000'000))); + env.close(); + amm.deposit({.account = carol, .tokens = 1'000}); + + // MPTokenIssuance doesn't exist amm.withdraw( - {.account = account, + WithdrawArg{ + .account = carol, + .asset1Out = badMPT(1), + .asset2Out = eur(1), + .assets = std::make_pair(badMPT, eur), + .err = Ter(terNO_AMM)}); + + // MPToken doesn't exist - doesn't apply since MPToken is created + // on withdraw in this case + + // MPTLock is set + usd.set({.flags = tfMPTLock}); + // carol and issuer can't withdraw + for (auto const& account : {carol, gw}) + { + amm.withdraw( + {.account = account, + .asset1Out = usd(1), + .asset2Out = eur(1), + .err = Ter(tecLOCKED)}); + amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecLOCKED)}); + // can single withdraw another asset + amm.withdraw( + {.account = account, + .asset1Out = eur(1), + .assets = std::make_pair(eur, usd)}); + } + usd.set({.flags = tfMPTUnlock}); + + // MPTRequireAuth is set + usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + usd.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize}); + // carol can't withdraw + amm.withdraw( + {.account = carol, .asset1Out = usd(1), .asset2Out = eur(1), - .err = Ter(tecLOCKED)}); - amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecLOCKED)}); - // can single withdraw another asset + .err = Ter(tecNO_AUTH)}); + // can withdraw another asset amm.withdraw( - {.account = account, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + // issuer can withdraw + amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)}); + // carol is authorized, can withdraw + usd.authorize({.account = gw, .holder = carol}); + amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); + + // MPTCanTransfer is not set, allow to withdraw + { + auto usd2 = makeNoTransferMPT({carol}, 1'000'000); + AMM amm2(env, gw, usd2(1'000), eur(1'000)); + + // carol cannot deposit usd2 without MPTCanTransfer, so give her + // LP tokens directly to test the withdraw path. + env.trust(STAmount{amm2.lptIssue(), 1'000}, carol); + env(pay(gw, carol, STAmount{amm2.lptIssue(), 100})); + env.close(); + + // carol can withdraw + amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)}); + // can withdraw another asset + amm2.withdraw( + {.account = carol, + .asset1Out = eur(1), + .assets = std::make_pair(eur, usd2)}); + // issuer can withdraw + amm2.withdraw({.account = gw, .asset1Out = usd2(1), .asset2Out = eur(1)}); + // Holder can't transfer to another holder + env.fund(XRP(1'000), bob); + usd2.authorize({.account = bob}); + env(pay(carol, bob, usd2(1)), Ter(tecNO_AUTH)); + usd2.authorize({.account = bob, .flags = tfMPTUnauthorize}); + // Can redeem + env(pay(carol, gw, usd2(1))); + usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + // carol can withdraw + amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)}); + } + + // MPToken created on withdraw + { + auto usd3 = makeDexMPT(); + auto eur3 = makeDexMPT({carol}, 1'000'000); + AMM amm3(env, gw, usd3(1'000), eur3(1'000)); + + BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)) == nullptr); + // single-deposit EUR + amm3.deposit( + {.account = carol, + .asset1In = eur3(1'000), + .assets = std::make_pair(eur3, usd3)}); + BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)) == nullptr); + // withdraw in USD to create MPToken + amm3.withdraw({.account = carol, .asset1Out = usd3(100)}); + BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol))); + } } - usd.set({.flags = tfMPTUnlock}); - - // MPTRequireAuth is set - - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); - usd.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize}); - // carol can't withdraw - amm.withdraw( - {.account = carol, - .asset1Out = usd(1), - .asset2Out = eur(1), - .err = Ter(tecNO_AUTH)}); - // can withdraw another asset - amm.withdraw( - {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); - // issuer can withdraw - amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)}); - // carol is authorized, can withdraw - usd.authorize({.account = gw, .holder = carol}); - amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); - - // MPTCanTransfer is not set, allow to withdraw - - usd.set({.mutableFlags = tmfMPTClearRequireAuth}); - usd.set({.mutableFlags = tmfMPTClearCanTransfer}); - // carol can withdraw - amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); - // can withdraw another asset - amm.withdraw( - {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); - // issuer can withdraw - amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)}); - // Holder can't transfer to another holder - env.fund(XRP(1'000), bob); - usd.authorize({.account = bob}); - env(pay(carol, bob, usd(1)), Ter(tecNO_AUTH)); - usd.authorize({.account = bob, .flags = tfMPTUnauthorize}); - // Can redeem - env(pay(carol, gw, usd(1))); - // carol can withdraw - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)}); - - usd.set({.mutableFlags = tmfMPTSetCanTransfer}); - - // MPTCanTrade is not set, allow to withdraw - - usd.set({.mutableFlags = tmfMPTClearCanTrade}); - amm.withdraw({.account = gw, .tokens = 1'000}); - amm.withdraw({.account = carol, .tokens = 1'000}); - // Can't DEX - amm.deposit( - DepositArg{.account = carol, .asset1In = usd(1), .err = Ter(tecNO_PERMISSION)}); - usd.set({.mutableFlags = tmfMPTSetCanTrade}); - - // MPToken created on withdraw - - // redeem all carol's USD and unauthorize USD - amm.withdrawAll(carol); - env(pay(carol, gw, env.balance(carol, usd))); - usd.authorize({.account = carol, .flags = tfMPTUnauthorize}); - BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), carol)) == nullptr); - // single-deposit EUR - amm.deposit( - {.account = carol, .asset1In = eur(1'000), .assets = std::make_pair(eur, usd)}); - // withdraw in USD to create MPToken - amm.withdraw({.account = carol, .asset1Out = usd(100)}); - BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), carol))); } } @@ -7716,41 +7614,37 @@ class MPToken_test : public beast::unit_test::Suite Env env(*this); env.fund(XRP(1'000), gw, alice, carol); - MPTTester mpt( - {.env = env, - .issuer = gw, - .holders = {alice, carol}, - .pay = 100, - .flags = kMptDexFlags, - .mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanTrade}); + auto const checkCanTradeCanTransfer = [&](std::uint32_t const flags, + TER const gwToGw, + TER const gwToAlice, + TER const aliceToAlice, + TER const aliceToCarol) { + MPTTester const mpt( + {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, .flags = flags}); + + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == gwToGw); + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == gwToAlice); + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == aliceToAlice); + BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == aliceToCarol); + }; // Both flags are enabled - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol))); + checkCanTradeCanTransfer(kMptDexFlags, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS); // MPTCanTrade is disabled - mpt.set({.mutableFlags = tmfMPTClearCanTrade}); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_PERMISSION); + checkCanTradeCanTransfer( + tfMPTCanTransfer, + tecNO_PERMISSION, + tecNO_PERMISSION, + tecNO_PERMISSION, + tecNO_PERMISSION); // MPTCanTransfer is disabled - mpt.set({.mutableFlags = tmfMPTSetCanTrade}); - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw))); - BEAST_EXPECT(isTesSuccess(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice))); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_AUTH); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_AUTH); + checkCanTradeCanTransfer(tfMPTCanTrade, tesSUCCESS, tesSUCCESS, tecNO_AUTH, tecNO_AUTH); // Both flags are disabled - mpt.set({.mutableFlags = tmfMPTClearCanTrade}); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == tecNO_PERMISSION); - BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == tecNO_PERMISSION); + checkCanTradeCanTransfer( + 0, tecNO_PERMISSION, tecNO_PERMISSION, tecNO_PERMISSION, tecNO_PERMISSION); } public: diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ad9a5f5996..0ffb8d9cc3 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -6,7 +6,6 @@ #include #include // IWYU pragma: keep #include -#include #include #include #include @@ -14,8 +13,6 @@ #include // IWYU pragma: keep #include #include -#include -#include #include #include #include @@ -426,17 +423,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite using namespace test::jtx; Account const alice{"alice"}; - Account const bob{"bob"}; Env env{*this, features}; - env.fund(XRP(1000), alice, bob); + env.fund(XRP(1000), alice); env.close(); // We're going to hack the ledger in order to avoid generating // 4 billion or so NFTs. Because we're hacking the ledger we - // need alice's account to have non-zero sfMintedNFTokens, - // sfBurnedNFTokens, sfSponsoredOwnerCount, sfSponsoringOwnerCount, - // sfSponsoringAccountCount fields. This prevents an exception when - // the AccountRoot template is applied. + // need alice's account to have non-zero sfMintedNFTokens and + // sfBurnedNFTokens fields. This prevents an exception when the + // AccountRoot template is applied. { uint256 const nftId0{token::getNextID(env, alice, 0u)}; env(token::mint(alice, 0u)); @@ -444,23 +439,6 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::burn(alice, nftId0)); env.close(); - - env(did::set(alice), - did::Uri("uri"), - sponsor::As(bob, spfSponsorReserve), - Sig(sfSponsorSignature, bob)); - env.close(); - - env(did::set(bob), - did::Uri("uri"), - sponsor::As(alice, spfSponsorReserve), - Sig(sfSponsorSignature, alice)); - env.close(); - - env(sponsor::transfer(bob, tfSponsorshipCreate), - sponsor::As(alice, spfSponsorReserve), - Sig(sfSponsorSignature, alice)); - env.close(); } // Note that we're bypassing almost all of the ledger's safety @@ -474,7 +452,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Just for sanity's sake we'll check that the current value // of sfMintedNFTokens matches what we expect. - auto replacement = std::make_shared(*sle, sle->key()); + auto replacement = std::make_shared(*sle); if (replacement->getFieldU32(sfMintedNFTokens) != 1) return false; // Unexpected test conditions. @@ -1142,6 +1120,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite if (features[fixCleanup3_1_3]) { buyerCount--; + BEAST_EXPECT(!env.closed()->exists(keylet::nftoffer(buyerExpOfferIndex))); } BEAST_EXPECT(ownerCount(env, buyer) == buyerCount); @@ -1165,6 +1144,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite if (features[fixCleanup3_1_3]) { aliceCount--; + BEAST_EXPECT(!env.closed()->exists(keylet::nftoffer(aliceExpOfferIndex))); } BEAST_EXPECT(ownerCount(env, alice) == aliceCount); BEAST_EXPECT(ownerCount(env, buyer) == buyerCount); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 5fc049cd1c..7d6acb29e3 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -798,11 +798,13 @@ public: // The offer expires (it's not removed yet). env.close(); env.require(Owners(bob, 1), offers(bob, 1)); + auto const expiredBobOffer = keylet::offer(bob, env.seq(bob) - 1); // bob creates the offer that will be crossed. env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS)); env.close(); env.require(Owners(bob, 2), offers(bob, 2)); + auto const crossedBobOffer = keylet::offer(bob, env.seq(bob) - 1); env(trust(alice, usd(1000)), Ter(tesSUCCESS)); env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS)); @@ -821,6 +823,8 @@ public: Balance(bob, usd(kNone)), Owners(bob, 1), offers(bob, 1)); + BEAST_EXPECT(!env.current()->exists(expiredBobOffer)); + BEAST_EXPECT(env.current()->exists(crossedBobOffer)); // Order that can be filled env(offer(alice, XRP(500), usd(500)), Txflags(tfFillOrKill), Ter(tesSUCCESS)); @@ -836,6 +840,27 @@ public: offers(bob, 0)); } + // A failed Fill-or-Kill may tentatively consume a funded offer before + // the transaction is reset. That offer must not be treated as an + // unfunded offer cleanup. + { + Env env{*this, features}; + + env.fund(startBalance, gw, alice, bob); + env.close(); + + env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS)); + env.close(); + auto const bobOffer = keylet::offer(bob, env.seq(bob) - 1); + + env(trust(alice, usd(1000)), Ter(tesSUCCESS)); + env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS)); + env(offer(alice, XRP(1000), usd(1000)), Txflags(tfFillOrKill), Ter(tecKILLED)); + + env.require(offers(alice, 0), offers(bob, 1), Balance(alice, usd(1000))); + BEAST_EXPECT(env.current()->exists(bobOffer)); + } + // Immediate or Cancel - cross as much as possible // and add nothing on the books: { diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 0b4222ca48..bb2d9636e6 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -1990,7 +1990,10 @@ public: run() override { using namespace test::jtx; - FeatureBitset const all{testableAmendments()}; + // fixCleanup3_2_0 changes payment-channel error codes (tem* -> tec*) + // and channel-closing semantics. This suite asserts the + // pre-amendment behavior, so run it with the amendment disabled. + FeatureBitset const all{testableAmendments() - fixCleanup3_2_0}; testWithFeats(all); testDepositAuthCreds(); testMetaAndOwnership(all - fixIncludeKeyletFields); diff --git a/src/test/app/SetRegularKey_test.cpp b/src/test/app/SetRegularKey_test.cpp index b5d1af9ef0..b512885606 100644 --- a/src/test/app/SetRegularKey_test.cpp +++ b/src/test/app/SetRegularKey_test.cpp @@ -72,6 +72,27 @@ public: env(regkey(alice, alice), Ter(temBAD_REGKEY)); } + void + testNoAlternativeKey() + { + using namespace test::jtx; + + testcase("Cannot remove last signing method"); + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(10000), alice); + + env(regkey(alice, bob)); + env(fset(alice, asfDisableMaster), Sig(alice)); + + env(regkey(alice, kDisabled), Sig(bob), Ter(tecNO_ALTERNATIVE_KEY)); + + auto const sle = env.le(alice); + BEAST_EXPECT( + sle && sle->isFlag(lsfDisableMaster) && sle->getAccountID(sfRegularKey) == bob.id()); + } + void testPasswordSpent() { @@ -169,6 +190,7 @@ public: { testDisabledMasterKey(); testDisabledRegularKey(); + testNoAlternativeKey(); testPasswordSpent(); testUniversalMask(); testTicketRegularKey(); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index c5399c71ca..d0f8a60654 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1,27 +1,24 @@ -#include #include #include -#include #include #include #include #include #include -#include #include #include #include -#include #include #include #include -#include +#include #include #include #include #include +#include #include -#include +#include #include #include #include @@ -31,9 +28,6 @@ #include #include #include -#include - -#include #include #include @@ -53,7 +47,6 @@ #include #include #include -#include #include #include #include @@ -61,14 +54,14 @@ #include #include #include +#include -#include #include #include #include #include +#include #include -#include #include #include @@ -322,8 +315,9 @@ public: env.close(); // Increasing feeAmount to reach insufficient reserve - auto const currentFeeAmount = - env.le(keylet::sponsor(sponsor.id(), alice.id()))->getFieldAmount(sfFeeAmount).xrp(); + auto const currentFeeAmount = env.le(keylet::sponsorship(sponsor.id(), alice.id())) + ->getFieldAmount(sfFeeAmount) + .xrp(); adjustAccountXRPBalance(env, sponsor, XRP(310)); env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)), sponsor::SponseeAcc(alice), @@ -421,8 +415,6 @@ public: Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); - Account const invalid("invalid"); - Account const signer1("signer1"); Account const signer2("signer2"); @@ -541,9 +533,9 @@ public: Ter(tesSUCCESS)); env.close(); - auto sle = env.le(keylet::sponsor(sponsor, alice)); + auto sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfReserveCount) == 100); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForFee)); @@ -557,9 +549,9 @@ public: Ter(tesSUCCESS)); env.close(); - sle = env.le(keylet::sponsor(sponsor, alice)); + sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfReserveCount) == 50); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 50); BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(50)); BEAST_EXPECT(sle->at(sfMaxFee) == XRP(0.5)); BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(2)); @@ -571,9 +563,9 @@ public: Ter(tesSUCCESS)); env.close(); - sle = env.le(keylet::sponsor(sponsor, alice)); + sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfReserveCount) == 200); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 200); BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(200)); BEAST_EXPECT(sle->at(sfMaxFee) == XRP(2)); BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(3)); @@ -597,7 +589,7 @@ public: // delete from sponsee env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); // create sponsorship with zero value env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), @@ -605,9 +597,9 @@ public: Fee(XRP(1))); env.close(); - sle = env.le(keylet::sponsor(sponsor, alice)); + sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee)); // verify flags from previous sponsorship are not carried over @@ -620,9 +612,9 @@ public: Fee(XRP(1))); env.close(); - sle = env.le(keylet::sponsor(sponsor, alice)); + sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfReserveCount) == 100); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); @@ -632,9 +624,9 @@ public: Fee(XRP(1))); env.close(); - sle = env.le(keylet::sponsor(sponsor, alice)); + sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee)); } @@ -695,12 +687,14 @@ public: testcase("PreFund and Cosign"); using namespace test::jtx; Account const alice("alice"); + Account const bob("bob"); + Account const charlie("charlie"); Account const sponsor("sponsor"); { - // both pre-funded and co-signed,pre-funded value is used + // Both pre-funded and co-signed; the pre-funded value is used. Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, sponsor); + env.fund(XRP(10000), alice, bob, sponsor); env.close(); env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), @@ -708,49 +702,52 @@ public: Ter(tesSUCCESS)); env.close(); - env(did::set(alice), - did::Uri("uri"), + auto const checkSeq = env.seq(alice); + env(check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), Sig(sfSponsorSignature, sponsor), Fee(XRP(1)), Ter(tesSUCCESS)); env.close(); - auto sle = env.le(keylet::sponsor(sponsor, alice)); + auto sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfReserveCount) == 99); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); - env(did::del(alice), Ter(tesSUCCESS)); + env(check::cancel(alice, keylet::check(alice, checkSeq).key), Ter(tesSUCCESS)); env.close(); - sle = env.le(keylet::sponsor(sponsor, alice)); + sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); - BEAST_EXPECT(sle->at(sfReserveCount) == 99); // not paybacked + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not restored BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); } { // if pre-funded value is not enough, error Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, sponsor); + env.fund(XRP(10000), alice, bob, charlie, sponsor); env.close(); - env(sponsor::set(sponsor, 0, 10, XRP(10), XRP(100)), + env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); env.close(); // Fee insufficient - env(ticket::create(alice, 1), + env(check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), Sig(sfSponsorSignature, sponsor), Fee(XRP(11)), Ter(terINSUF_FEE_B)); env.close(); + env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + // reserve insufficient - env(ticket::create(alice, 11), + env(check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), Sig(sfSponsorSignature, sponsor), Fee(XRP(1)), @@ -759,6 +756,47 @@ public: } } + void + testSponsoredFreeTierReserve() + { + testcase("Sponsored Free-Tier Reserve"); + using namespace test::jtx; + Account const alice("alice"); + Account const issuer("issuer"); + Account const sponsor("sponsor"); + + // Trust lines and MPTokens normally skip the reserve check when the + // holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the + // tx is sponsored, that shortcut must not apply — the sponsor must + // still cover the reserve. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, issuer); + // Sponsor is funded just below the reserve required to cover a single + // sponsored item. + env.fund(reserve(env, 1) - drops(1), sponsor); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + MPTTester mptt(env, issuer, {.fund = false}); + mptt.create(); + + // Free-tier trust line cosigned by an undercapitalized sponsor must + // fail — the holder's free-first-two-items shortcut does not let the + // sponsor skip the reserve check. + env(trust(alice, issuer["USD"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + // Free-tier MPTokenAuthorize must also fail for the same reason. + env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + void testTransferSponsor() { @@ -771,8 +809,7 @@ public: Account const alice("alice"); Account const bob("bob"); Account const sponsor1("sponsor1"); - Account const sponsor2("sponsor2"); - env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + env.fund(XRP(10000), alice, bob, sponsor1); env.close(); env(sponsor::transfer( @@ -822,19 +859,21 @@ public: Env env{*this, testableAmendments()}; Account const alice("alice"); Account const bob("bob"); + Account const charlie("charlie"); Account const sponsor("sponsor"); env.fund(XRP(10000), alice, bob, sponsor); env.close(); { // sponsor object - env(did::set(alice), - did::Uri("uri"), + env.fund(XRP(1000), charlie); + env.close(); + env(deposit::auth(alice, charlie), sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor)); env.close(); - auto const keylet = keylet::did(alice); + auto const keylet = keylet::depositPreauth(alice, charlie); env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); @@ -1199,8 +1238,8 @@ public: auto checkSle = env.le(keylet::unchecked(checkId)); BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor1.id()); - auto sponsor1Sle = env.le(keylet::sponsor(sponsor1, alice)); - BEAST_EXPECT(sponsor1Sle->getFieldU32(sfReserveCount) == 99); + auto sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); // transfer sponsor env(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(alice)); @@ -1222,10 +1261,10 @@ public: checkSle = env.le(keylet::unchecked(checkId)); BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor)); BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor2.id()); - sponsor1Sle = env.le(keylet::sponsor(sponsor1, alice)); - BEAST_EXPECT(sponsor1Sle->getFieldU32(sfReserveCount) == 99); - auto sponsor2Sle = env.le(keylet::sponsor(sponsor2, alice)); - BEAST_EXPECT(sponsor2Sle->getFieldU32(sfReserveCount) == 99); + sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + auto sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99); // dissolve sponsor adjustAccountXRPBalance(env, alice, reserve(env, 1)); @@ -1245,8 +1284,8 @@ public: !env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount)); checkSle = env.le(keylet::unchecked(checkId)); BEAST_EXPECT(!checkSle->isFieldPresent(sfSponsor)); - sponsor2Sle = env.le(keylet::sponsor(sponsor2, alice)); - BEAST_EXPECT(sponsor2Sle->getFieldU32(sfReserveCount) == 99); + sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99); } { @@ -1319,7 +1358,8 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); BEAST_EXPECT( - env.le(keylet::sponsor(sponsor, alice))->getFieldU32(sfReserveCount) == 100); + env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == + 100); // not the owner of the object env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION)); @@ -1333,7 +1373,8 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); BEAST_EXPECT( - env.le(keylet::sponsor(sponsor, alice))->getFieldU32(sfReserveCount) == 100); + env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == + 100); } { @@ -1424,6 +1465,73 @@ public: Ter(tecNO_PERMISSION)); } } + + { + // existing owner objects that are outside the v1 SponsorshipTransfer + // object allow-list + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + auto const checkBlocked = [&](Account const& account, uint256 const& objectID) { + env(sponsor::transfer(account, tfSponsorshipCreate, objectID), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_PERMISSION)); + env.close(); + }; + + auto const ticketSeq = env.seq(alice); + env(ticket::create(alice, 1)); + env.close(); + auto const ticketID = keylet::TicketT()(alice, ticketSeq + 1).key; + BEAST_EXPECT(env.le(keylet::unchecked(ticketID))); + checkBlocked(alice, ticketID); + + env(did::setValid(alice)); + env.close(); + auto const didKeylet = keylet::did(alice.id()); + BEAST_EXPECT(env.le(didKeylet)); + checkBlocked(alice, didKeylet.key); + + env(token::mint(alice, 0u)); + env.close(); + auto const nftPageKeylet = keylet::nftpageMax(alice); + BEAST_EXPECT(env.le(nftPageKeylet)); + checkBlocked(alice, nftPageKeylet.key); + + Account const borrower("borrower"); + env.fund(XRP(1000000), borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset}); + env(vaultTx); + env.close(); + + env(vault.deposit( + {.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)})); + env.close(); + + auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); + env(loanBroker::set(alice, vaultKeylet.key), + loanBroker::kDebtMaximum(xrpAsset(1000).value()), + loanBroker::kManagementFeeRate(TenthBips16{0}), + loanBroker::kCoverRateMinimum(TenthBips32{0}), + loanBroker::kCoverRateLiquidation(TenthBips32{0})); + env.close(); + + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()), + Sig(sfCounterpartySignature, alice), + Fee(env.current()->fees().base * 2)); + env.close(); + BEAST_EXPECT(env.le(loanKeylet)); + checkBlocked(borrower, loanKeylet.key); + } } void @@ -1443,10 +1551,8 @@ public: env.close(); { - // Fee should be checked before permission check, - // otherwise tecNO_SPONSOR_PERMISSION returned when permission - // check fails could cause context reset to pay Fee because it - // is tec error + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto sponsorBalance = env.balance(sponsor); @@ -1523,8 +1629,6 @@ public: { // below reserve adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve); - env.close(); - auto const feeAmt = XRP(4); env(noop(alice), Fee(env.current()->fees().base), @@ -1552,14 +1656,14 @@ public: env.close(); auto const sponsorFeeBalance = [&](Account const& sponsor, Account const& sponsee) { - return env.le(keylet::sponsor(sponsor, sponsee))->getFieldAmount(sfFeeAmount).xrp(); + return env.le(keylet::sponsorship(sponsor, sponsee)) + ->getFieldAmount(sfFeeAmount) + .xrp(); }; { - // Fee should be checked before permission check, - // otherwise tecNO_SPONSOR_PERMISSION returned when permission - // check fails could cause context reset to pay Fee because it - // is tec error + // Fee should be checked before sponsor permission, otherwise a tec + // result from a later check could cause context reset to pay Fee. auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto sponsorBalance = env.balance(sponsor); @@ -1632,7 +1736,7 @@ public: BEAST_EXPECT(env.balance(bob) == bobBalance + XRP(100)); BEAST_EXPECT(env.balance(sponsor) == sponsorBalance); BEAST_EXPECT( - !env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + !env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); } // reset FeeAmount and MaxFee @@ -1689,14 +1793,16 @@ public: env(sponsor::set_fee(sponsor, 0, XRP(10)), sponsor::SponseeAcc(alice)); env.close(); - BEAST_EXPECT(env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); auto sponsorAvailableFee = sponsorFeeBalance(sponsor, alice); env(check::cancel(alice, uint256(1)), Fee(sponsorAvailableFee), sponsor::As(sponsor, spfSponsorFee), Ter(tecNO_ENTRY)); env.close(); - BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT( + !env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); } } @@ -1730,12 +1836,72 @@ public: BEAST_EXPECT(result.applied); // Only MaxFee (10 drops) must be deducted, not the full 1000 drops. - auto const sle = overlay.read(keylet::sponsor(carol.id(), alice.id())); + auto const sle = overlay.read(keylet::sponsorship(carol.id(), alice.id())); BEAST_EXPECT(sle); BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount)); BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10) } + // LedgerStateFix charges an owner-reserve fee and can claim that fee + // while returning tecFAILED_PROCESSING. That path must be safe when the + // fee is pre-funded by a sponsorship object. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + env(ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee), + Ter(tecFAILED_PROCESSING)); + + if (auto const sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle)) + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + } + + // If preclaim saw spendable sponsored FeeAmount but the apply view no + // longer has it, the fee path should fail cleanly instead of throwing. + { + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(1000), alice, sponsor); + env.close(); + + auto const fixFee = drops(env.current()->fees().increment); + env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); + env.close(); + + OpenView overlay(&*env.closed()); + auto jt = env.jt( + ledgerStateFix::nftPageLinks(alice, alice), + Fee(fixFee), + sponsor::As(sponsor, spfSponsorFee)); + + auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + auto const pc = preclaim(pf, env.app(), overlay); + BEAST_EXPECT(isTesSuccess(pc.ter)); + + auto const original = overlay.read(keylet::sponsorship(sponsor, alice)); + if (BEAST_EXPECT(original)) + { + auto sle = std::make_shared(*original); + sle->makeFieldAbsent(sfFeeAmount); + overlay.rawReplace(sle); + } + + auto const result = doApply(pc, env.app(), overlay); + BEAST_EXPECT(result.ter == terINSUF_FEE_B); + BEAST_EXPECT(!result.applied); + } + // test lsfSponsorshipRequireSignForFee { Env env{*this, testableAmendments()}; @@ -1757,7 +1923,8 @@ public: env.close(); BEAST_EXPECT( - env.le(keylet::sponsor(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(10)); + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == + XRP(10)); // clear flag env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), @@ -1765,7 +1932,7 @@ public: env.close(); // Payment is re-applied - BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); } // RequireSignForFee: co-signing should succeed @@ -1798,7 +1965,7 @@ public: env.close(); BEAST_EXPECT( - env.le(keylet::sponsor(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(9)); + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(9)); } } @@ -1848,6 +2015,20 @@ public: env(pay(alice, bob, usd(100)), Txflags(tfSponsorCreatedAccount), Ter(temBAD_AMOUNT)); env.close(); + // Sponsored account creation is reserve sponsorship and is only supported for direct XRP + // payments. + env(pay(alice, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Sendmax(usd(2)), + Ter(temINVALID)); + env.close(); + + env(pay(alice, bob, drops(1)), + Txflags(tfSponsorCreatedAccount), + Path(~XRP), + Ter(temINVALID)); + env.close(); + // Account is not sponsored by normal Sponsor specification { env(pay(alice, bob, drops(baseAccountReserve(*env.current(), 0))), @@ -1974,7 +2155,8 @@ public: BEAST_EXPECT(ownerCount(env, alice) == 0); BEAST_EXPECT( - env.le(keylet::sponsor(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(10)); + env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == + XRP(10)); // clear flag env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), @@ -1983,7 +2165,7 @@ public: // CheckCreate is re-applied BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(!env.le(keylet::sponsor(sponsor, alice))->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount)); } } @@ -1994,25 +2176,25 @@ public: using namespace test::jtx; Env env{*this, testableAmendments()}; Account const alice("alice"); + Account const bob("bob"); Account const sponsor("sponsor"); - env.fund(XRP(10000), alice, sponsor); + env.fund(XRP(10000), alice, bob, sponsor); env.close(); - // test Sufficient sponsor balance if (cosigning) { - adjustAccountXRPBalance(env, sponsor, reserve(env, 99)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1)); - env(ticket::create(alice, 100), + env(check::create(alice, bob, XRP(100)), sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), Ter(tecINSUFFICIENT_RESERVE)); env.close(); - adjustAccountXRPBalance(env, sponsor, reserve(env, 100)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); - env(ticket::create(alice, 100), + env(check::create(alice, bob, XRP(100)), sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), Ter(tesSUCCESS)); @@ -2023,16 +2205,16 @@ public: env(sponsor::set_reserve(sponsor, 0, 250), sponsor::SponseeAcc(alice)); env.close(); - adjustAccountXRPBalance(env, sponsor, reserve(env, 99 + 1 /* sponsor object*/)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); - env(ticket::create(alice, 100), + env(check::create(alice, bob, XRP(100)), sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUFFICIENT_RESERVE)); env.close(); - adjustAccountXRPBalance(env, sponsor, reserve(env, 100 + 1 /* sponsor object*/)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); - env(ticket::create(alice, 100), + env(check::create(alice, bob, XRP(100)), sponsor::As(sponsor, spfSponsorReserve), Ter(tesSUCCESS)); env.close(); @@ -2054,10 +2236,11 @@ public: std::optional> expected = std::nullopt) { using namespace test::jtx; - // auto const sponsorOwnerCountBefore = ownerCount(env, sponsor); auto const sponseeOwnerCountBefore = ownerCount(env, sponsee); auto const sponseeSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsee); auto const sponseeSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsee); + auto const sponsorOwnerCountBefore = ownerCount(env, sponsor); + auto const sponsorSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsor); auto const sponsorSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsor); std::optional sponsorSig = @@ -2091,13 +2274,13 @@ public: else { // cleanup previous sponsorship - if (env.le(keylet::sponsor(sponsor, sponsee))) + if (env.le(keylet::sponsorship(sponsor, sponsee))) { env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); env.close(); } - if (sponsorReserveCount - 1 > 0) + if (sponsorReserveCount > 1) { env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)), sponsor::SponseeAcc(sponsee)); @@ -2110,8 +2293,53 @@ public: } env.close(); } + + // A failed sponsored create must not consume prefunded reserve or mutate owner counts. + auto const sponseeOwnerCountBeforeAttempt = ownerCount(env, sponsee); + auto const sponseeSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsee); + auto const sponseeSponsoringOwnerCountBeforeAttempt = + sponsoringOwnerCount(env, sponsee); + auto const sponsorOwnerCountBeforeAttempt = ownerCount(env, sponsor); + auto const sponsorSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsor); + auto const sponsorSponsoringOwnerCountBeforeAttempt = + sponsoringOwnerCount(env, sponsor); + auto const sponsorshipSleBeforeAttempt = env.le(keylet::sponsorship(sponsor, sponsee)); + bool const reserveCountPresentBeforeAttempt = sponsorshipSleBeforeAttempt && + sponsorshipSleBeforeAttempt->isFieldPresent(sfRemainingOwnerCount); + std::uint32_t const reserveCountBeforeAttempt = reserveCountPresentBeforeAttempt + ? sponsorshipSleBeforeAttempt->getFieldU32(sfRemainingOwnerCount) + : 0; + callback(env, submit(insufficientReserveResult)); env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == sponseeOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsee) == sponseeSponsoredOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsee) == sponseeSponsoringOwnerCountBeforeAttempt); + BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBeforeAttempt); + BEAST_EXPECT( + sponsoringOwnerCount(env, sponsor) == sponsorSponsoringOwnerCountBeforeAttempt); + + auto const sponsorshipSleAfterAttempt = env.le(keylet::sponsorship(sponsor, sponsee)); + BEAST_EXPECT( + static_cast(sponsorshipSleAfterAttempt) == + static_cast(sponsorshipSleBeforeAttempt)); + if (sponsorshipSleAfterAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->isFieldPresent(sfRemainingOwnerCount) == + reserveCountPresentBeforeAttempt); + if (reserveCountPresentBeforeAttempt) + { + BEAST_EXPECT( + sponsorshipSleAfterAttempt->getFieldU32(sfRemainingOwnerCount) == + reserveCountBeforeAttempt); + } + } } // Success @@ -2134,6 +2362,13 @@ public: if (!cosigning) { + // Prefunded success consumes the reserved owner slot before cleanup. + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, sponsee)); + BEAST_EXPECT(sponsorshipSle); + BEAST_EXPECT( + !sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) || + sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0); + // cleanup sponsorship env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee)); env.close(); @@ -2152,411 +2387,14 @@ public: sponsorReserveCount); BEAST_EXPECT( sponsoringOwnerCount(env, sponsee) - sponseeSponsoringOwnerCountBefore == 0); + BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBefore); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBefore); BEAST_EXPECT( sponsoringOwnerCount(env, sponsor) - sponsorSponsoringOwnerCountBefore == sponsorReserveCount); } }; - void - testAMM(bool cosigning) - { - testcase("AMM"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const gw("gw"); - Account const sponsor("sponsor"); - - auto const usd = gw["usd"]; - auto const eur = gw["eur"]; - - auto const ammCreate = [&](Env& env, - Account const& account, - STAmount const& amount1, - STAmount const& amount2) { - json::Value jv; - jv[jss::TransactionType] = jss::AMMCreate; - jv[jss::Account] = account.human(); - jv[jss::Amount] = amount1.getJson(JsonOptions::Values::None); - jv[jss::Amount2] = amount2.getJson(JsonOptions::Values::None); - jv[jss::TradingFee] = 0; - jv[jss::Fee] = std::to_string(env.current()->fees().increment.drops()); - return jv; - }; - - auto const ammDeposit = [&](Env& env, - Account const& account, - STAmount const& amount1, - STAmount const& amount2) { - json::Value jv; - jv[jss::TransactionType] = jss::AMMDeposit; - jv[jss::Account] = account.human(); - jv[jss::Asset] = STIssue(sfAsset, amount1.asset()).getJson(JsonOptions::Values::None); - jv[jss::Asset2] = STIssue(sfAsset, amount2.asset()).getJson(JsonOptions::Values::None); - jv[jss::Amount] = amount1.value().getJson(JsonOptions::Values::None); - jv[jss::Amount2] = amount2.value().getJson(JsonOptions::Values::None); - jv[jss::Flags] = tfTwoAsset; - return jv; - }; - - { - // AMMCreate - // - sponsor LPToken - // - doesn't sponsor AMM object - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, gw, sponsor); - env.close(); - - env(trust(alice, usd(10000))); - env(trust(alice, eur(10000))); - env.close(); - - env(pay(gw, alice, usd(1000))); - env(pay(gw, alice, eur(1000))); - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUF_RESERVE_LINE, - [&](Env& env, auto const& submit) { - submit(ammCreate(env, alice, usd(100), eur(100))); - }, - [&]() { - auto const amm = env.current()->read(keylet::amm(usd.issue(), eur.issue())); - auto const ammAccount = Account("amm", amm->getAccountID(sfAccount)); - BEAST_EXPECT(ownerCount(env, alice) == 3); // RippleState (usd,eur/LP Token) - BEAST_EXPECT(ownerCount(env, ammAccount) == 2); // usd, eur - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); // LPToken - BEAST_EXPECT(sponsoredOwnerCount(env, ammAccount) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // LPToken - BEAST_EXPECT( - !env.le(keylet::amm(usd.issue(), eur.issue()))->isFieldPresent(sfSponsor)); - }); - - auto const ammKeylet = keylet::amm(usd.issue(), eur.issue()); - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipCreate, ammKeylet.key), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor), - Ter(tecNO_PERMISSION)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); - env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); - env(sponsor::transfer(alice, tfSponsorshipCreate, ammKeylet.key), - sponsor::As(sponsor, spfSponsorReserve), - Ter(tecNO_PERMISSION)); - env.close(); - } - } - { - // AMMDeposit - // - sponsor new LPToken - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, gw, sponsor); - env.close(); - - env(trust(alice, usd(10000))); - env(trust(alice, eur(10000))); - env(trust(bob, usd(10000))); - env(trust(bob, eur(10000))); - env.close(); - - env(pay(gw, alice, usd(1000))); - env(pay(gw, alice, eur(1000))); - env(pay(gw, bob, usd(1000))); - env(pay(gw, bob, eur(1000))); - env.close(); - - env(ammCreate(env, alice, usd(100), eur(100))); - env.close(); - - BEAST_EXPECT(ownerCount(env, bob) == 2); // RippleState (usd,eur) - - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecINSUF_RESERVE_LINE, - [&](Env& env, auto const& submit) { - submit(ammDeposit(env, bob, usd(100), eur(100))); - }); - } - { - // AMMDeposit single-asset XRP: reserve sponsor covers LP trustline reserve - // but depositor's own liquid XRP is insufficient for the deposit → tecUNFUNDED_AMM - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, gw, sponsor); - env.close(); - - env(trust(bob, usd(10000))); - env(trust(alice, usd(10000))); - env.close(); - env(pay(gw, bob, usd(1000))); - env.close(); - - AMM const amm(env, bob, XRP(1000), usd(100)); - - // alice has 1 owner object (usd trust line); give her reserve + 5 XRP liquid - adjustAccountXRPBalance(env, alice, reserve(env, ownerCount(env, alice)) + XRP(5)); - - auto const jv = AMM::depositJv( - {.account = alice, - .asset1In = XRP(10), - .assets = std::make_pair(Asset{xrpIssue()}, Asset{usd.issue()})}); - - if (cosigning) - { - env(jv, - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor), - Ter(tecINSUF_RESERVE_LINE)); - } - else - { - env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUF_RESERVE_LINE)); - env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); - } - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); // no LP token was created - } - { - // AMMWithdraw - { - // Single Asset Withdraw - // - sponsor new RippleState - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, gw, sponsor); - env.close(); - - env(trust(alice, usd(10000))); - env(trust(alice, eur(10000))); - env.close(); - - env(pay(gw, alice, usd(1000))); - env(pay(gw, alice, eur(1000))); - env.close(); - - env(ammCreate(env, alice, usd(1000), eur(1000)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - env(trust(alice, usd(0))); - env(trust(alice, eur(0))); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); // LPToken - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); // LPToken - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // LPToken - - json::Value jv; - jv[jss::TransactionType] = jss::AMMWithdraw; - jv[jss::Account] = alice.human(); - jv[jss::Asset] = STIssue(sfAsset, usd.issue()).getJson(JsonOptions::Values::None); - jv[jss::Asset2] = STIssue(sfAsset, eur.issue()).getJson(JsonOptions::Values::None); - jv[jss::Amount] = usd(100).value().getJson(JsonOptions::Values::None); - jv[jss::Flags] = tfSingleAsset; - - env(ticket::create(sponsor, 1)); // adjust for free - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(jv); }); - } - { - // Double Asset Withdraw - // - sponsor new RippleState * 2 - // - remove sponsored LPToken - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, gw, sponsor); - env.close(); - - env(trust(alice, usd(10000))); - env(trust(alice, eur(10000))); - env.close(); - - env(pay(gw, alice, usd(1000))); - env(pay(gw, alice, eur(1000))); - env.close(); - - env(ammCreate(env, alice, usd(1000), eur(1000)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - env(trust(alice, usd(0))); - env(trust(alice, eur(0))); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); // LPToken - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - - json::Value jv; - jv[jss::TransactionType] = jss::AMMWithdraw; - jv[jss::Account] = alice.human(); - jv[jss::Asset] = STIssue(sfAsset, usd.issue()).getJson(JsonOptions::Values::None); - jv[jss::Asset2] = STIssue(sfAsset, eur.issue()).getJson(JsonOptions::Values::None); - jv[jss::Flags] = tfWithdrawAll; - - env(ticket::create(sponsor, 1)); // adjust for free trustline - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 2, - 2, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(jv); }, - [&]() { - // LPToken deleted, usd, eur created - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); - }); - } - } - { - // AMMClawback - // - doesn't sponsor holder's new RippleState - // - remove sponsored LPToken - Account const gw2("gw2"); - auto const eur2 = gw2["eur"]; - - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, gw, gw2, sponsor); - env.close(); - - env(fset(gw, asfAllowTrustLineClawback)); - env.close(); - - env(trust(alice, usd(10000))); - env(trust(alice, eur2(10000))); - env.close(); - - env(pay(gw, alice, usd(100))); - env(pay(gw2, alice, eur2(100))); - env.close(); - - env(ammCreate(env, alice, usd(100), eur2(100)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - env(trust(alice, usd(0))); - env(trust(alice, eur2(0))); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); // LPToken - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - { - // doesn't sponsor holder's new RippleState - env(amm::ammClawback(gw, alice, usd, eur2, usd(10)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 2); // LPToken, eur2 - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - } - { - // remove sponsored LPToken - env(amm::ammClawback(gw, alice, usd, eur2, std::nullopt)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); // eur2 - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - } - { - // AMMDelete - // - remove sponsored LPToken trustlines - Env env( - *this, - envconfig([](std::unique_ptr cfg) { - cfg->fees.referenceFee = XRPAmount(1); - return cfg; - }), - testableAmendments()); - env.fund(XRP(20'000), alice, gw, sponsor); - env.close(); - env(trust(alice, usd(10'000))); - env.close(); - env(pay(gw, alice, usd(10'000))); - env.close(); - - AMM amm(env, gw, XRP(10'000), usd(10'000)); - for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 2) + 10; ++i) - { - Account const a{std::to_string(i)}; - env.fund(XRP(1'000), a); - if (cosigning) - { - env(trust(a, STAmount{amm.lptIssue(), 10'000}), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(a)); - env.close(); - env(trust(a, STAmount{amm.lptIssue(), 10'000}), - sponsor::As(sponsor, spfSponsorReserve)); - env.close(); - } - } - - BEAST_EXPECT( - sponsoringOwnerCount(env, sponsor) == ((kMaxDeletableAmmTrustLines * 2) + 10)); - - // The trustlines are partially deleted. - amm.withdrawAll(gw); - BEAST_EXPECT(amm.ammExists()); - - // AMMDelete has to be called twice to delete AMM. - amm.ammDelete(alice, Ter(tecINCOMPLETE)); - BEAST_EXPECT(amm.ammExists()); - - // Deletes remaining trustlines and deletes AMM. - amm.ammDelete(alice); - BEAST_EXPECT(!amm.ammExists()); - BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount()))); - - BEAST_EXPECT( - !env.le(keylet::account(sponsor))->isFieldPresent(sfSponsoringAccountCount)); - } - } - void testCheck(bool cosigning) { @@ -2575,7 +2413,7 @@ public: env.fund(XRP(10000), alice, bob, sponsor, sponsor2); env.close(); - // CheckCreate -> Check = 0Cancel + // CheckCreate -> Check -> CheckCancel uint32_t seq = 0; testEachSponsorship( @@ -2641,7 +2479,7 @@ public: env.fund(XRP(10000), alice, bob, sponsor); env.close(); - // CheckCreate -> = 0 CheckCash + // CheckCreate -> CheckCash uint32_t seq2 = 0; testEachSponsorship( env, @@ -2682,7 +2520,7 @@ public: env(pay(gw, alice, usd(100))); env.close(); - // CheckCreat = 0e -> CheckCash + // CheckCreate -> CheckCash uint32_t seq2 = 0; testEachSponsorship( env, @@ -2723,425 +2561,6 @@ public: } } - void - testOffer(bool cosigning) - { - testcase("Offer"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const gw("gw"); - Account const sponsor1("sponsor1"); - Account const sponsor2("sponsor2"); - - auto usd = gw["usd"]; - auto eur = gw["eur"]; - - { - Env env{*this, testableAmendments()}; - - env.fund(XRP(10000), alice, gw, sponsor1, sponsor2); - env.close(); - - // OfferCreate - uint32_t seq = 0; - testEachSponsorship( - env, - cosigning, - sponsor1, - alice, - 1, - 1, - tecINSUF_RESERVE_OFFER, - [&](Env& env, auto const& submit) { - seq = env.seq(alice); - submit(offer(alice, usd(1), XRP(1))); - }); - - // transfer sponsor - auto const keylet = keylet::offer(alice, seq); - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); - - // OfferCancel - env(offerCancel(alice, seq)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - - { - Env env{*this, testableAmendments()}; - - env.fund(XRP(10000), alice, gw, sponsor1, sponsor2); - env.close(); - - // OfferCreate - uint32_t seq = 0; - testEachSponsorship( - env, - cosigning, - sponsor1, - alice, - 1, - 1, - tecINSUF_RESERVE_OFFER, - [&](Env& env, auto const& submit) { - seq = env.seq(alice); - submit(offer(alice, usd(1), XRP(1))); - }); - - // OfferCreate with Cancel (new sponsor) - auto const seq2 = env.seq(alice); - if (cosigning) - { - env(offer(alice, usd(1), XRP(1)), - Json(jss::OfferSequence, seq), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(offer(alice, usd(1), XRP(1)), - Json(jss::OfferSequence, seq), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // OfferCreate with Cancel (no sponsor) - env(offer(alice, usd(1), XRP(1)), Json(jss::OfferSequence, seq2)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - - // test Offer Execution doesn't sponsor new trustline - { - Env env{*this, testableAmendments()}; - env.fund(XRP(10000), alice, bob, gw, sponsor1, sponsor2); - env.close(); - - env(trust(alice, usd(100))); - env(trust(bob, eur(100))); - env.close(); - - env(pay(gw, alice, usd(100))); - env(pay(gw, bob, eur(100))); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(ownerCount(env, bob) == 1); - - // OfferCreate - if (cosigning) - { - env(offer(alice, eur(1), usd(1)), - sponsor::As(sponsor1, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor1)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor1, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(offer(alice, eur(1), usd(1)), sponsor::As(sponsor1, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); - - BEAST_EXPECT(ownerCount(env, bob) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - - // OfferCreate (cross offer) - if (cosigning) - { - env(offer(bob, usd(1), eur(1)), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(bob)); - env.close(); - - env(offer(bob, usd(1), eur(1)), sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0); - - // does not sponsor new trustline by cross offer - BEAST_EXPECT(ownerCount(env, bob) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - } - - void - testTicket(bool cosigning) - { - testcase("Ticket"); - using namespace test::jtx; - Account const alice("alice"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, sponsor, sponsor2); - env.close(); - - // TicketCreate - uint32_t ticketSeq = 0; - - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 250, - 250, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - ticketSeq = env.seq(alice) + 1; - submit(ticket::create(alice, 250)); - }); - - auto const keylet = keylet::TicketT()(alice, ticketSeq); - BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); - - // transfer sponsor - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 250); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 250); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 249); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id()); - - // use a Ticket - env(noop(alice), ticket::Use(ticketSeq)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 249); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 249); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 249); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - } - - void - testCredentials(bool cosigning) - { - testcase("Credentials"); - using namespace test::jtx; - Account const issuer("issuer"); - Account const subject("subject"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - auto const credType = std::string("credType"); - auto const credTypeSlice = Slice(credType.data(), credType.size()); - - // CredentialsCreate - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), issuer, subject, sponsor, sponsor2); - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - issuer, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(credentials::create(subject, issuer, credType), credentials::Uri("uri")); - }); - - BEAST_EXPECT(ownerCount(env, subject) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); - - // transfer sponsor - auto const keylet = keylet::credential(subject, issuer, credTypeSlice); - if (cosigning) - { - env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(issuer)); - env.close(); - - env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, subject) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // CredentialsAccept - testEachSponsorship( - env, - cosigning, - sponsor, - subject, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(credentials::accept(subject, issuer, credType)); - }); - - BEAST_EXPECT(ownerCount(env, issuer) == 0); - BEAST_EXPECT(ownerCount(env, subject) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - - // transfer accepted credential - if (cosigning) - { - env(sponsor::transfer(subject, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(subject)); - env.close(); - - env(sponsor::transfer(subject, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - // CredentialsDelete - env(credentials::deleteCred(subject, subject, issuer, credType)); - env.close(); - - BEAST_EXPECT(ownerCount(env, issuer) == 0); - BEAST_EXPECT(ownerCount(env, subject) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), issuer, subject, sponsor); - env.close(); - - // Accept Sponsored Credentials without sponsoring - testEachSponsorship( - env, - cosigning, - sponsor, - issuer, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(credentials::create(subject, issuer, credType)); - }); - - env(credentials::accept(subject, issuer, credType)); - env.close(); - - // sponsorship is removed - BEAST_EXPECT(ownerCount(env, issuer) == 0); - BEAST_EXPECT(ownerCount(env, subject) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, issuer) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, subject) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(!env.le(keylet::credential(subject, issuer, credTypeSlice)) - ->isFieldPresent(sfSponsor)); - - env(credentials::deleteCred(subject, subject, issuer, credType)); - env.close(); - } - } - void testDelegate(bool cosigning) { @@ -3212,6 +2631,7 @@ public: Account const alice("alice"); Account const sponsor("sponsor"); Account const sponsor2("sponsor2"); + auto const credType = std::string("credType"); { Env env{*this, testableAmendments()}; @@ -3242,10 +2662,19 @@ public: { env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); env.close(); + // No sponsor signature here: this exercises the prefunded reassign path. env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); + sponsor::As(sponsor2, spfSponsorReserve)); env.close(); + + auto const sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsor2Sle); + if (sponsor2Sle) + { + BEAST_EXPECT( + !sponsor2Sle->isFieldPresent(sfRemainingOwnerCount) || + sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 0); + } } BEAST_EXPECT(ownerCount(env, alice) == 1); @@ -3262,6 +2691,45 @@ public: BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); } + + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, sponsor); + env.close(); + auto const authCreds = std::vector{ + {.issuer = sponsor, .credType = credType}}; + auto const preauthKeylet = keylet::depositPreauth( + alice.id(), + std::set>{ + {sponsor.id(), Slice(credType.data(), credType.size())}}); + + // Cover DepositPreauth's sfAuthorizeCredentials sponsor-reserve branch. + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env&, auto const& submit) { + submit(deposit::authCredentials(alice, authCreds)); + }); + + // Cover sfUnauthorizeCredentials cleanup for a sponsored preauth object. + BEAST_EXPECT(env.le(preauthKeylet)); + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + env(deposit::unauthCredentials(alice, authCreds)); + env.close(); + + BEAST_EXPECT(!env.le(preauthKeylet)); + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } } void @@ -3311,6 +2779,17 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + if (!cosigning) + { + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor2, alice)); + BEAST_EXPECT(sponsorshipSle); + if (sponsorshipSle) + { + BEAST_EXPECT( + !sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) || + sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0); + } + } // DIDDelete env(did::del(alice)); @@ -3654,14 +3133,11 @@ public: env(jv); env.close(); - // for free mptoken checks - // adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); + // Create tickets so the sponsor is past free-tier reserve behavior. std::uint32_t const ticketSeq{env.seq(sponsor) + 1}; env(ticket::create(sponsor, 2)); env.close(); - // adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - - // drops(1)); jv = {}; jv[sfTransactionType] = jss::MPTokenAuthorize; jv[sfAccount] = bob.human(); @@ -3688,10 +3164,11 @@ public: env(noop(sponsor), ticket::Use(ticketSeq)); env.close(); - // pass (free mptoken) + // pass (free-tier mptoken for the holder, but the sponsor is still + // charged a reserve increment regardless of the ownerCount < 2 shortcut). if (cosigning) { - adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); env(jv, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), @@ -3708,357 +3185,6 @@ public: } } - void - testNFToken(bool cosigning) - { - testcase("NFToken"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - { - Env env{*this, testableAmendments()}; - - env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); - env.close(); - - // NFTokenMint - uint256 nftId; - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - nftId = token::getNextID(env, alice, 0); - submit(token::mint(alice)); - }); - - // transfer sponsor - auto const keylet = keylet::nftpageMax(alice); - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - } - // NFTokenBurn - env(token::burn(alice, nftId)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - - // NFTokenMintOffer - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 2, - 2, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(token::mint(alice), token::Amount(XRP(100))); - }); - } - - { - // multiple nft page process - Env env{*this, testableAmendments()}; - - env.fund(XRP(1000000), alice, bob, sponsor); - env.close(); - - auto const nftCount = 200; - - // NFTokenMint - if (cosigning) - { - for (auto i = 0; i < nftCount; i++) - { - env(token::mint(alice), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - } - } - else - { - env(sponsor::set_reserve(sponsor, 0, 8), sponsor::SponseeAcc(alice)); - env.close(); - for (auto i = 0; i < nftCount; i++) - { - env(token::mint(alice), sponsor::As(sponsor, spfSponsorReserve)); - } - } - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == sponsoredOwnerCount(env, alice)); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == sponsoringOwnerCount(env, sponsor)); - - // NFTokenBurn - for (auto i = 0; i < nftCount; i++) - { - auto const nftId = token::getID(env, alice, 0, i, 0, 0); - env(token::burn(alice, nftId)); - } - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - } - - void - testNFTokenOffer(bool cosigning) - { - testcase("NFTokenOffer"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const broker("broker"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - auto const taxon = 0u; - - { - // Mint + CreateOffer + CancelOffer - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); - env.close(); - - // Mint - uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; - env(token::mint(alice, taxon), Txflags(tfTransferable)); - env.close(); - - // NFTokenOfferCreate - uint256 offerIndex1; - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - offerIndex1 = keylet::nftoffer(alice, env.seq(alice)).key; - submit( - token::createOffer(alice, nftId, XRP(1)), - token::Destination(bob), - Txflags(tfSellNFToken)); - }); - - uint256 offerIndex2; - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - offerIndex2 = keylet::nftoffer(alice, env.seq(alice)).key; - submit( - token::createOffer(alice, nftId, XRP(1)), - token::Destination(bob), - Txflags(tfSellNFToken)); - }); - - // transfer sponsor - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, offerIndex1), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(sponsor::transfer(alice, tfSponsorshipReassign, offerIndex1), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 3); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // NFTokenOfferCancel - env(token::cancelOffer(alice, {offerIndex1, offerIndex2})); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - - { - // Mint + CreateSellOffer + AcceptSellOffer - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, sponsor); - env.close(); - - // Mint - uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; - env(token::mint(alice, taxon), Txflags(tfTransferable)); - env.close(); - - // NFTokenOfferCreate - uint256 offerIndex; - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - offerIndex = keylet::nftoffer(alice, env.seq(alice)).key; - submit( - token::createOffer(alice, nftId, XRP(1)), - token::Destination(bob), - Txflags(tfSellNFToken)); - }); - - // NFTokenOfferAccept - env(token::acceptSellOffer(bob, offerIndex)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, bob) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - - { - // Mint + CreateBuyOffer + AcceptBuyOffer - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, sponsor); - env.close(); - - // Mint - uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; - env(token::mint(alice, taxon), Txflags(tfTransferable)); - env.close(); - - // NFTokenOfferCreate - uint256 offerIndex; - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - offerIndex = keylet::nftoffer(bob, env.seq(bob)).key; - submit( - token::createOffer(bob, nftId, XRP(1)), - token::Owner(alice), - token::Destination(alice)); - }); - - // NFTokenOfferAccept - env(token::acceptBuyOffer(alice, offerIndex)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, bob) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - { - // Broker - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, broker, sponsor, sponsor2); - env.close(); - - // Mint - uint256 const nftId{token::getNextID(env, alice, taxon, tfTransferable)}; - env(token::mint(alice, taxon), Txflags(tfTransferable)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); - - // NFTokenOfferCreate (BuyOffer) - uint256 buyOfferIndex; - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - buyOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key; - submit( - token::createOffer(bob, nftId, XRP(1)), - token::Owner(alice), - token::Destination(broker)); - }); - - // NFTokenOfferCreate (SellOffer) - uint256 sellOfferIndex; - testEachSponsorship( - env, - cosigning, - sponsor2, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - sellOfferIndex = keylet::nftoffer(alice, env.seq(alice)).key; - submit( - token::createOffer(alice, nftId, XRP(1)), - Txflags(tfSellNFToken), - token::Destination(broker)); - }); - - // NFTokenOfferAccept - env(token::brokerOffers(broker, buyOfferIndex, sellOfferIndex)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, bob) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - } - void testPayChan(bool cosigning) { @@ -4127,382 +3253,6 @@ public: } } - void - testPermissionedDomain(bool cosigning) - { - testcase("PermissionedDomain"); - using namespace test::jtx; - Account const alice("alice"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, sponsor, sponsor2); - env.close(); - - // PermissionedDomainSet - pdomain::Credentials credentials{{.issuer = alice, .credType = "first credential"}}; - uint32_t seq = 0; - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - seq = env.seq(alice); - submit(pdomain::setTx(alice, credentials)); - }); - - // transfer sponsor - auto const keylet = keylet::permissionedDomain(alice, seq); - - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // PermissionedDomainDelete - auto objects = pdomain::getObjects(alice, env); - auto const domain = objects.begin()->first; - env(pdomain::deleteTx(alice, domain)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - } - - void - testOracle(bool cosigning) - { - testcase("Oracle"); - using namespace test::jtx; - using namespace std::chrono; - using DataSeries = - std::vector>; - - Account const alice("alice"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - auto const oracleSet = [](Env& env, Account const& account, uint8_t dataSeriesSize) { - auto const now = env.timeKeeper().now(); - env.close(now + oracle::kTestStartTime - kEpochOffset); - json::Value jv; - jv[jss::TransactionType] = jss::OracleSet; - jv[jss::Account] = to_string(account); - jv[jss::OracleDocumentID] = 1; - jv[jss::LastUpdateTime] = to_string( - duration_cast(env.current()->header().closeTime.time_since_epoch()) - .count() + - kEpochOffset.count() + 100); - jv[jss::PriceDataSeries] = json::ValueType::Array; - jv[jss::Provider] = strHex(std::string{"provider"}); - jv[jss::AssetClass] = strHex(std::string{"currency"}); - - DataSeries const series = { - {"XRP", "US1", 740, 1}, - {"XRP", "US2", 750, 1}, - {"XRP", "US3", 740, 1}, - {"XRP", "US4", 750, 1}, - {"XRP", "US5", 740, 1}, - {"XRP", "US6", 750, 1}, - {"XRP", "US7", 740, 1}, - {"XRP", "US8", 750, 1}, - {"XRP", "US9", 740, 1}, - {"XRP", "U10", 750, 1}, - }; - - DataSeries const actualSeries(series.begin(), series.begin() + dataSeriesSize); - - json::Value dataSeries(json::ValueType::Array); - for (auto const& data : actualSeries) - { - json::Value priceData; - json::Value price; - price[jss::BaseAsset] = std::get<0>(data); - price[jss::QuoteAsset] = std::get<1>(data); - price[jss::AssetPrice] = std::get<2>(data); - price[jss::Scale] = std::get<3>(data); - priceData[jss::PriceData] = price; - dataSeries.append(priceData); - } - jv[jss::PriceDataSeries] = dataSeries; - return jv; - }; - - auto const oracleDelete = [&](Account const& account) { - json::Value jv; - jv[jss::TransactionType] = jss::OracleDelete; - jv[jss::Account] = to_string(account); - jv[jss::OracleDocumentID] = 1; - return jv; - }; - - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, sponsor, sponsor2); - env.close(); - - { - // OracleSet (reserve 1) - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 5)); }); - - // transfer sponsor - auto const keylet = keylet::oracle(alice, 1); - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // OracleDelete - env(oracleDelete(alice)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - { - // OracleSet (reserve 2) - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 2, - 2, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 6)); }); - - // transfer sponsor - auto const keylet = keylet::oracle(alice, 1); - if (cosigning) - { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 2), sponsor::SponseeAcc(alice)); - env.close(); - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); - - // OracleDelete - env(oracleDelete(alice)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - { - // OracleSet (reserve 1->2, sponsor1 -> no-sponsor) - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 5)); }); - - // reserve 1->2 - env(oracleSet(env, alice, 6)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - - // OracleDelete - env(oracleDelete(alice)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - { - // OracleSet (reserve 1->2, sponsor1 -> sponsor2) - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 5)); }); - // return; - - // reserve 1->2 - testEachSponsorship( - env, - cosigning, - sponsor2, - alice, - 1, - 2, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 6)); }, - [&]() { - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2); - }); - - // OracleDelete - env(oracleDelete(alice)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - { - // OracleSet (reserve 1->2, non-sponsor -> sponsor1) - env(oracleSet(env, alice, 5)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 1); - - // reserve 1->2 - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 2, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(oracleSet(env, alice, 6)); }); - - // OracleDelete - env(oracleDelete(alice)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - for (bool const isTwoOwnerCount : {false, true}) - { - // test sponsor transfer - auto const dataSeriesSize = isTwoOwnerCount ? 6 : 5; - auto const ocount = isTwoOwnerCount ? 2 : 1; - - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - ocount, - ocount, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(oracleSet(env, alice, dataSeriesSize)); - }); - - // transfer sponsor - if (cosigning) - { - env(sponsor::transfer( - alice, tfSponsorshipReassign, keylet::oracle(alice, 1).key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, ocount), sponsor::SponseeAcc(alice)); - env.close(); - env(sponsor::transfer( - alice, tfSponsorshipReassign, keylet::oracle(alice, 1).key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == ocount); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == ocount); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == ocount); - - // dissolve sponsor - env(sponsor::transfer(alice, tfSponsorshipEnd, keylet::oracle(alice, 1).key)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == ocount); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - - // remove sponsor - env(oracleDelete(alice)); - env.close(); - } - } - } - void testSignerList(bool cosigning) { @@ -4574,6 +3324,105 @@ public: BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); } + void + testSponsoredTrustLineNoFreeReserve() + { + // An account with ownerCount < 2 may create its first trust lines even + // without meeting the reserve. In any case, the sponsor pays the full + // reserve in all cases, even for the sponsee's very first trust line. + testcase("Sponsored trust line gets no free-reserve exception"); + using namespace test::jtx; + + Account const issuer("issuer"); + Account const alice("alice"); + Account const sponsor("sponsor"); + + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), issuer, alice, sponsor); + env.close(); + + auto const usd = issuer["usd"]; + auto const lineKeylet = keylet::line(alice, issuer, usd.currency); + + // Sponsor funded for exactly its base reserve + adjustAccountXRPBalance(env, sponsor, reserve(env, 0)); + + // alice's ownerCount is 0, so an unsponsored first trust line would be + // free; but because it is sponsored, the reserve check is enforced + // against the sponsor, which is one increment short. + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + BEAST_EXPECT(!env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + + // Give the sponsor has exactly one owner-reserve increment; the same + // sponsored first trust line now succeeds and the sponsor pays for it. + adjustAccountXRPBalance(env, sponsor, reserve(env, 1)); + + env(trust(alice, usd(100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + BEAST_EXPECT(env.le(lineKeylet)); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(ownerCount(env, alice) == 1); + } + + void + testCoSignReserveBoundedBySponsorshipBudget() + { + // sponsor co-signs, so a fee-only object (ReserveCount == 0) makes a co-signed + // reserve sponsorship fail -- with no fallback to the sponsor's balance. + testcase("Co-signed reserve sponsorship is bounded by Sponsorship budget"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const sponsor("sponsor"); + Account const sponsee("sponsee"); + env.fund(XRP(10000), sponsor, sponsee); + env.close(); + + // Prefund a FEE-only Sponsorship for the sponsee; ReserveCount + // defaults to 0. + env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + BEAST_EXPECT(env.le(keylet::sponsorship(sponsor, sponsee))); + + // Sponsee creates a Check with the sponsor co-signing the reserve. The + // fee-only Sponsorship's has ReserveCount (0), so this fails + // with tecINSUFFICIENT_RESERVE + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 0); + + // Bumping the Sponsorship's ReserveCount budget makes the same + // co-signed reserve sponsorship succeed, the budget is what gates it. + env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(sponsee)); + env.close(); + + env(check::create(sponsee, sponsor, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(ownerCount(env, sponsee) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 1); + } + void testTrustSet(bool cosigning) { @@ -4764,688 +3613,6 @@ public: } } - void - testVault(bool cosigning) - { - testcase("Vault"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const gw("gw"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - Asset asset = gw["IOU"].asset(); - - // VaultCreate - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, gw, sponsor); - env.close(); - - Vault const vault{env}; - auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - - env(ticket::create(sponsor, 2)); - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 3, // Vault, PseudoAccount, MPToken(Share Token) - 3, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - auto result = vault.create({.owner = alice, .asset = asset}); - submit(std::get<0>(result)); - keylet = std::get<1>(result); - }); - BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); - } - // VaultDeposit - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, gw, sponsor); - env.close(); - - Vault const vault{env}; - auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - - env(trust(bob, asset(1000))); - env.close(); - env(pay(gw, bob, asset(1000))); - env.close(); - - BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState - - auto const depositTx = - vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}); - - env(ticket::create(sponsor, 2)); // for free MPToken - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(depositTx); }); - } - // VaultWithdraw - { - // RippleState Vault - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, gw, sponsor); - env.close(); - - Vault const vault{env}; - auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - - env(trust(bob, asset(100))); - env.close(); - env(pay(gw, bob, asset(100))); - env.close(); - - auto const depositTx = - vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}); - - env(ticket::create(sponsor, 2)); // for free MPToken - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(depositTx); }); - - env(trust(bob, asset(0))); // remove trustline - env.close(); - - BEAST_EXPECT(ownerCount(env, bob) == 1); // MPToken(share) - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); // MPToken(share) - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // MPToken(share) - - // create Trustline with vault withdraw - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecNO_LINE_INSUF_RESERVE, - [&](Env& env, auto const& submit) { - submit(vault.withdraw( - {.depositor = bob, .id = keylet.key, .amount = asset(50)})); - }); - - BEAST_EXPECT(ownerCount(env, bob) == 2); // RippleState, MPToken(share) - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 2); // RippleState, MPToken(share) - BEAST_EXPECT( - sponsoringOwnerCount(env, sponsor) == 2); // RippleState, MPToken(share) - - // remove sponsored MPToken(share) - env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = asset(50)})); - env.close(); - - BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); // RippleState - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // RippleState - } - // MPToken Vault - { - // VaultWithdraw doesn't create MPToken for depositor - } - } - // VaultClawback - { - // remove sponsored shares MPToken - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, gw, sponsor); - env.close(); - - env(fset(gw, asfAllowTrustLineClawback)); - env.close(); - - Vault const vault{env}; - auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - - env(trust(bob, asset(100))); - env.close(); - env(pay(gw, bob, asset(100))); - env.close(); - - auto const depositTx = - vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}); - - env(ticket::create(sponsor, 2)); // for free MPToken - env.close(); - - testEachSponsorship( - env, - cosigning, - sponsor, - bob, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { submit(depositTx); }); - - BEAST_EXPECT(ownerCount(env, bob) == 2); // RippleState, MPToken(share) - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); // MPToken(share) - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // MPToken(share) - - env(vault.clawback({.issuer = gw, .id = keylet.key, .holder = bob, .amount = asset(0)}), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState - BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - // VaultDelete - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, gw, sponsor); - env.close(); - - env(fset(gw, asfAllowTrustLineClawback)); - env.close(); - - Vault const vault{env}; - auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 3); // Vault, PseudoAccount, MPToken(share) - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 3); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 3); - - env(vault.del({.owner = alice, .id = keylet.key})); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - } - } - - void - testXChain(bool cosigning) - { - testcase("XChain"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const doorA("doorA"); - Account const signer("signer"); - Account const sponsor("sponsor"); - - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, sponsor, doorA); - env.close(); - - auto jvb = bridge(doorA, XRP, env.master, XRP); - - env(signers(doorA, 1, {signer})); - env.close(); - - // XChainCreateBridge - { - testEachSponsorship( - env, - cosigning, - sponsor, - doorA, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(bridgeCreate(doorA, jvb, XRP(1), XRP(1))); - }); - } - // XChainCreateClaimID - { - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(xchainCreateClaimId(alice, jvb, XRP(1), bob)); - }); - } - // XChainCommit - { - BEAST_EXPECT(ownerCount(env, alice) == 1); // XChainOwnedClaimID - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); - - if (cosigning) - { - env(xchainCommit(alice, jvb, 1, XRP(100), bob), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(xchainCommit(alice, jvb, 1, XRP(100), bob), - sponsor::As(sponsor, spfSponsorReserve)); - env.close(); - - env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); - env.close(); - } - - // doesn't sponsor anything - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); - } - // XChainAddClaimAttestation - { - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); - - if (cosigning) - { - env(claimAttestation(alice, jvb, bob, XRP(1), bob, false, 1, bob, signer), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - env(claimAttestation(alice, jvb, bob, XRP(1), bob, false, 1, bob, signer), - sponsor::As(sponsor, spfSponsorReserve)); - env.close(); - - env(sponsor::del(sponsor), sponsor::SponseeAcc(alice)); - env.close(); - } - - // XChainOwnedClaimID deleted - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - } - // XChainClaim - { - // prepare for claim - { - env(xchainCreateClaimId(alice, jvb, XRP(1), bob), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env(xchainCommit(alice, jvb, 2, XRP(100))); // omit destination - env(claimAttestation( - alice, jvb, bob, XRP(100), bob, false, 2, std::nullopt, signer)); - env.close(); - } - - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2); - - env(xchainClaim(alice, jvb, 2, XRP(100), bob)); - env.close(); - - // XChainOwnedClaimID deleted - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - } - // XChainCreateAccountClaimID - { - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, doorA) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, doorA) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - - env(createAccountAttestation( - alice, jvb, alice, XRP(20), XRP(0), bob, false, 2, bob, signer), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor), - Ter(tesSUCCESS)); - env.close(); - - // XChainCreateAccountClaimID not sponsored - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(ownerCount(env, doorA) == 3); - BEAST_EXPECT(sponsoredOwnerCount(env, doorA) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - } - } - - void - testLending(bool cosigning) - { - testcase("Lending"); - using namespace test::jtx; - Account const alice("alice"); - Account const bob("bob"); - Account const issuer("issuer"); - Account const sponsor("sponsor"); - Account const sponsor2("sponsor2"); - - // LoanBrokerSet / LoanBrokerDelete - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); - env.close(); - - PrettyAsset const asset{xrpIssue(), 1'000'000}; - - Vault const vault{env}; - auto const [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - - BEAST_EXPECT( - ownerCount(env, alice) == 3); // Vault, PseudoAccount(Vault), MPToken(Vault) - - // LoanBrokerSet - testEachSponsorship( - // Both the Pseudo-account and LoanBroker objects are created, but only the - // LoanBroker is sponsored. - env, - cosigning, - sponsor, - alice, - 2, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(loanBroker::set(alice, keylet.key, 0)); - }); - - BEAST_EXPECT( - ownerCount(env, alice) == - 5); // LoanBroker, PseudoAccount(LB), (Vault, PseudoAccount(Vault), MPToken(Vault)) - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - - auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice) - 1); - - if (cosigning) - { - // transfer sponsor - env(sponsor::transfer(alice, tfSponsorshipReassign, brokerKeylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - // transfer sponsor - env(sponsor::transfer(alice, tfSponsorshipReassign, brokerKeylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - BEAST_EXPECT( - ownerCount(env, alice) == - 5); // LoanBroker, PseudoAccount(LB), (Vault, PseudoAccount(Vault), MPToken(Vault)) - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // LoanBrokerDelete - env(loanBroker::del(alice, brokerKeylet.key, 0)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 3); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); - } - - // LoanBrokerConverDeposit/Withdraw/Clawback - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000), alice, bob, issuer, sponsor); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - env.close(); - PrettyAsset const asset = mptt["MPT"]; - mptt.authorize({.account = alice}); - env.close(); - - env(pay(issuer, alice, asset(100))); - env.close(); - BEAST_EXPECT(ownerCount(env, alice) == 1); - - Vault const vault{env}; - auto const [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - - env(loanBroker::set(alice, keylet.key, 0)); - env.close(); - BEAST_EXPECT( - ownerCount(env, alice) == - 6); // LoanBroker, PseudoAccount(LB), (Vault, PseudoAccount(Vault), - // MPToken(Vault), MPToken(issuer)) - - auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice) - 1); - // LoanBrokerCoverDeposit - // doesn't sponsor anything - env(loanBroker::coverDeposit(alice, brokerKeylet.key, asset(100)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - BEAST_EXPECT(ownerCount(env, alice) == 6); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - - // remove MPToken(issuer) - mptt.authorize({.account = alice, .flags = tfMPTUnauthorize}); - env.close(); - BEAST_EXPECT(ownerCount(env, alice) == 5); - - env(ticket::create(sponsor, 2)); // for avoid free MPToken - env.close(); - - // LoanBrokerCoverWithdraw - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit(loanBroker::coverWithdraw(alice, brokerKeylet.key, asset(10))); - }); - - BEAST_EXPECT(ownerCount(env, alice) == 6); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - - // LoanBrokerCoverClawback - // doesn't sponsor anything - env(loanBroker::coverClawback(issuer), - loanBroker::kLoanBrokerId(brokerKeylet.key), - kAmount(asset(1)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - BEAST_EXPECT(ownerCount(env, alice) == 6); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - } - // LoanSet - { - Env env{*this, testableAmendments()}; - env.fund(XRP(1000000), alice, bob, issuer, sponsor, sponsor2); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - env.close(); - PrettyAsset const asset = mptt["MPT"]; - mptt.authorize({.account = alice}); - mptt.authorize({.account = bob}); - env.close(); - - env(pay(issuer, alice, asset(1000))); - env(pay(issuer, bob, asset(1000))); - env.close(); - - Vault const vault{env}; - auto const [tx, keylet] = vault.create({.owner = bob, .asset = asset}); - env(tx); - env.close(); - env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)})); - env.close(); - - auto const brokerKeylet = keylet::loanbroker(bob.id(), env.seq(bob)); - env(loanBroker::set(bob, keylet.key, 0)); - env.close(); - env(loanBroker::coverDeposit(bob, brokerKeylet.key, asset(100))); - env.close(); - - auto broker = env.le(brokerKeylet); - BEAST_EXPECT(broker->getFieldU32(sfOwnerCount) == 0); - BEAST_EXPECT(!broker->isFieldPresent(sfSponsoredOwnerCount)); - BEAST_EXPECT(!broker->isFieldPresent(sfSponsoringOwnerCount)); - - auto const loanSeq = broker->getFieldU32(sfLoanSequence); - testEachSponsorship( - env, - cosigning, - sponsor, - alice, - 1, - 1, - tecINSUFFICIENT_RESERVE, - [&](Env& env, auto const& submit) { - submit( - loan::set(alice, brokerKeylet.key, 10), - Sig(sfCounterpartySignature, bob), - Fee(XRP(1))); - }); - broker = env.le(brokerKeylet); - // broker'object doesn't sponsored - BEAST_EXPECT(broker->getFieldU32(sfOwnerCount) == 1); - BEAST_EXPECT(!broker->isFieldPresent(sfSponsoredOwnerCount)); - BEAST_EXPECT(!broker->isFieldPresent(sfSponsoringOwnerCount)); - - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSeq); - - auto sponsorSle = env.le(keylet::account(sponsor)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); - BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) == 1); - - // LoanManage - env(loan::manage(bob, loanKeylet.key, lsfLoanImpaired), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - // doesn't sponsor anything - sponsorSle = env.le(keylet::account(sponsor)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); - BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) == 1); - - // LoanPay - env(loan::pay(alice, loanKeylet.key, asset(10)), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - // doesn't sponsor anything - sponsorSle = env.le(keylet::account(sponsor)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); - BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) == 1); - - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - - // before transfer - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - - if (cosigning) - { - // transfer sponsor - env(sponsor::transfer(alice, tfSponsorshipReassign, loanKeylet.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env.close(); - } - else - { - env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); - env.close(); - - // transfer sponsor - env(sponsor::transfer(alice, tfSponsorshipReassign, loanKeylet.key), - sponsor::As(sponsor2, spfSponsorReserve)); - env.close(); - } - - // after transfer - BEAST_EXPECT(ownerCount(env, alice) == 2); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); - - // LoanDelete - env(loan::del(alice, loanKeylet.key), - sponsor::As(sponsor, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor)); - env.close(); - - // Sponsored ltLoan is deleted - BEAST_EXPECT(ownerCount(env, alice) == 1); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - // Sponsor for ltLoan object is deleted - sponsorSle = env.le(keylet::account(sponsor)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfOwnerCount) == 0); - BEAST_EXPECT(!sponsorSle->isFieldPresent(sfSponsoredOwnerCount)); - } - } - void testAccountDelete() { @@ -5469,7 +3636,7 @@ public: incLgrSeqForAccDel(env, sponsor); - auto const keylet = keylet::sponsor(sponsor, alice); + auto const keylet = keylet::sponsorship(sponsor, alice); auto const sponsorObj = env.le(keylet); BEAST_EXPECT(sponsorObj); @@ -5496,6 +3663,12 @@ public: auto const requiredFee = drops(env.current()->fees().increment); env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecNO_SPONSOR_PERMISSION)); + // The failed delete must leave the account sponsored by the original sponsor. + auto const aliceSle = env.le(keylet::account(alice)); + BEAST_EXPECT(aliceSle); + if (aliceSle) + BEAST_EXPECT(aliceSle->getAccountID(sfSponsor) == sponsor.id()); + auto const sponsorSle = env.le(keylet::account(sponsor)); BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); @@ -5539,13 +3712,19 @@ public: // Verify sfSponsoringOwnerCount is set on sponsor auto const sponsorSle = env.le(keylet::account(sponsor)); BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) >= 1); + auto const sponsoringOwnerCount = sponsorSle->getFieldU32(sfSponsoringOwnerCount); + BEAST_EXPECT(sponsoringOwnerCount >= 1); incLgrSeqForAccDel(env, sponsor); // AccountDelete should fail auto const requiredFee = drops(env.current()->fees().increment); env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + // The failed delete must not decrement the outstanding sponsored-object count. + auto const sponsorSleAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringOwnerCount)); + BEAST_EXPECT( + sponsorSleAfter->getFieldU32(sfSponsoringOwnerCount) == sponsoringOwnerCount); } { @@ -5562,13 +3741,19 @@ public: // Verify sfSponsoringAccountCount is set on sponsor auto const sponsorSle = env.le(keylet::account(sponsor)); BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount)); - BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); + auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount); + BEAST_EXPECT(sponsoringAccountCount == 1); incLgrSeqForAccDel(env, sponsor); // AccountDelete should fail auto const requiredFee = drops(env.current()->fees().increment); env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS)); + // The failed delete must not decrement the outstanding sponsored-account count. + auto const sponsorSleAfter = env.le(keylet::account(sponsor)); + BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringAccountCount)); + BEAST_EXPECT( + sponsorSleAfter->getFieldU32(sfSponsoringAccountCount) == sponsoringAccountCount); } } @@ -5821,10 +4006,10 @@ public: BEAST_EXPECT(env.balance(alice) == XRP(1000)); BEAST_EXPECT(env.balance(sponsor) == XRP(900)); - auto const sponsorshipSle = env.le(keylet::sponsor(sponsor, alice)); + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sponsorshipSle); BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100 - 1)); - BEAST_EXPECT(sponsorshipSle->at(sfReserveCount) == 100); + BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 100); } // // Inner transaction @@ -5883,7 +4068,7 @@ public: jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = ""; auto const seq = env.seq(alice); - // should fail BatchSigners does have signer for SponsorSignature + // should fail because BatchSigners does not have signer for SponsorSignature env(batch::outer(alice, seq, XRP(1), tfAllOrNothing), batch::Inner(jt.jv, seq + 1), batch::Inner(ticket::create(alice, 1), seq + 2), @@ -5906,7 +4091,8 @@ public: BEAST_EXPECT(env.balance(sponsor) == XRP(900)); auto jt = env.jtnofill( - ticket::create(alice, 1), sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee)); + check::create(alice, bob, XRP(1)), + sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee)); // remove txn signature since it is filled by env.jtnofill() jt.jv.removeMember(jss::TxnSignature); @@ -5927,10 +4113,10 @@ public: BEAST_EXPECT(env.balance(sponsor) == XRP(900)); // reserve count is decreased - auto const sponsorshipSle = env.le(keylet::sponsor(sponsor, alice)); + auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sponsorshipSle); BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100)); - BEAST_EXPECT(sponsorshipSle->at(sfReserveCount) == 99); + BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 99); } { @@ -5940,7 +4126,7 @@ public: env.close(); auto jt = env.jtnofill( - ticket::create(alice, 1), + check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee), Sig(sfSponsorSignature, sponsor)); // remove txn signature since it is filled by env.jtnofill() @@ -5967,31 +4153,53 @@ public: } } + // Verify that the central allow-list in preflight1Sponsor rejects + // spfSponsorReserve for transaction types that v1 does not permit. + void + testReserveSponsorGate() + { + testcase("Reserve sponsor allow-list gate"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, bob, sponsor); + env.close(); + + env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice)); + env.close(); + + auto checkBlocked = [&](json::Value const& jv) { + env(jv, + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temINVALID_FLAG)); + }; + + checkBlocked(ticket::create(alice, 1)); + checkBlocked(offer(alice, XRP(100), bob["USD"](100))); + checkBlocked(did::setValid(alice)); + checkBlocked(token::mint(alice, 0u)); + checkBlocked(sponsor::set(alice, 0, 10, XRP(10))); + checkBlocked(acctdelete(alice, bob)); + checkBlocked(loan::set(alice, uint256(1), Number{1})); + } + void testSponsorReserve(bool cosigning) { testRequireFlag(); testSponsorReserveSimple(cosigning); - testAMM(cosigning); testCheck(cosigning); - testOffer(cosigning); - testTicket(cosigning); - testCredentials(cosigning); testDelegate(cosigning); testDepositPreauth(cosigning); - testDID(cosigning); testEscrow(cosigning); testMPToken(cosigning); - testNFToken(cosigning); - testNFTokenOffer(cosigning); testPayChan(cosigning); - testPermissionedDomain(cosigning); - testOracle(cosigning); testSignerList(cosigning); testTrustSet(cosigning); - testVault(cosigning); - testXChain(cosigning); - testLending(cosigning); } protected: @@ -6010,6 +4218,7 @@ protected: testSimpleSponsorshipSet(); testPreFundAndCosign(); + testSponsoredFreeTierReserve(); testTransferSponsor(); testSponsorFee(); @@ -6019,6 +4228,10 @@ protected: testDelegatePermission(); testBatch(); + + testSponsoredTrustLineNoFreeReserve(); + testCoSignReserveBoundedBySponsorshipBudget(); + testReserveSponsorGate(); } void diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 0ae6b4d80a..330706dca6 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -2335,6 +2337,43 @@ public: BEAST_EXPECT(env.balance(alice) == drops(5)); } + void + testSponsorTxCannotQueue() + { + using namespace jtx; + testcase("disallow sponsored transaction from being queued"); + + Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}})); + + auto sponsor = Account("sponsor"); + auto sponsee = Account("sponsee"); + auto filler = Account("filler"); + + env.fund(XRP(50000), noripple(sponsor, sponsee)); + env.close(); + env.fund(XRP(50000), noripple(filler)); + env.close(); + + fillQueue(env, filler); + checkMetrics(*this, env, 0, 6, 4, 3); + + // Sponsored transactions are not allowed to be queued. + env(noop(sponsee), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Ter(telCAN_NOT_QUEUE)); + checkMetrics(*this, env, 0, 6, 4, 3); + + // Sponsored transactions may still apply directly if they pay the + // open ledger fee. They just cannot be held in the queue. + env(noop(sponsee), + sponsor::As(sponsor, spfSponsorFee), + Sig(sfSponsorSignature, sponsor), + Fee(openLedgerCost(env)), + Ter(tesSUCCESS)); + checkMetrics(*this, env, 0, 6, 5, 3); + } + void testConsequences() { @@ -4662,6 +4701,7 @@ public: testBlockersSeq(); testBlockersTicket(); testInFlightBalance(); + testSponsorTxCannotQueue(); testConsequences(); } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 2f43add2c8..3c58fa8142 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1599,7 +1599,7 @@ class Vault_test : public beast::unit_test::Suite {.flags = tfMPTCanTransfer | tfMPTCanLock | (args.enableClawback ? tfMPTCanClawback : kNone) | (args.requireAuth ? tfMPTRequireAuth : kNone), - .mutableFlags = tmfMPTCanMutateCanTransfer}); + .mutableFlags = tmfMPTCanEnableCanTransfer}); PrettyAsset const asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); @@ -2238,149 +2238,6 @@ class Vault_test : public beast::unit_test::Suite env.close(); } - testCase([this]( - Env& env, - Account const&, - Account const& owner, - Account const& depositor, - PrettyAsset const& asset, - Vault& vault, - MPTTester& mptt) { - testcase("MPT non-transferable: block deposit, allow withdraw"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx); - env.close(); - - // Issuer governance: clear CanTransfer. New exposure must be - // blocked, but recovery paths must remain open so existing - // depositors are not trapped. - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // New deposit is blocked. - env(tx, Ter{tecNO_AUTH}); - env.close(); - - // Existing depositor can always withdraw, even though the asset - // is no longer freely transferable. - tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx); - env.close(); - - // Delete vault with zero balance - env(vault.del({.owner = owner, .id = keylet.key})); - }); - - { - testcase("MPT non-transferable: pre-fixCleanup3_2_0 withdraw blocked"); - - // Regression: before fixCleanup3_2_0 a depositor was trapped if - // the issuer cleared lsfMPTCanTransfer. Verify that the legacy - // (broken) behavior is preserved when the amendment is disabled. - Env env{*this, testableAmendments() - fixCleanup3_2_0}; - Account const issuer{"issuer"}; - Account const owner{"owner"}; - Account const depositor{"depositor"}; - env.fund(XRP(10'000), issuer, owner, depositor); - env.close(); - Vault const vault{env}; - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mptt.issuanceID(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = depositor}); - env(pay(issuer, depositor, asset(1'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)})); - env.close(); - - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Pre-amendment: deposit blocked (matches new behavior). - env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}), - Ter{tecNO_AUTH}); - env.close(); - - // Pre-amendment: withdraw is also blocked - this is the bug - // that fixCleanup3_2_0 fixes. - env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}), - Ter{tecNO_AUTH}); - env.close(); - } - - { - testcase("MPT non-transferable: vault shares inherit restriction"); - - Env env{*this, testableAmendments()}; - Account const issuer{"issuer"}; - Account const owner{"owner"}; - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10'000), issuer, owner, alice, bob); - env.close(); - Vault const vault{env}; - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mptt.issuanceID(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = alice}); - mptt.authorize({.account = bob}); - env(pay(issuer, alice, asset(1'000))); - env(pay(issuer, bob, asset(1'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)})); - // Bob also deposits so he has a share MPToken to receive into. - env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)})); - env.close(); - - auto const shares = [&]() -> PrettyAsset { - auto const sle = env.le(keylet); - BEAST_EXPECT(sle != nullptr); - return MPTIssue(sle->at(sfShareMPTID)); - }(); - - // Sanity: while CanTransfer is set on the underlying, peer-to-peer - // share transfers are allowed. - env(pay(alice, bob, shares(1))); - env.close(); - - // Issuer governance: clear CanTransfer on the underlying. - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Vault shares inherit the restriction: third-party share-to-share - // payments are blocked. - env(pay(alice, bob, shares(1)), Ter{tecNO_AUTH}); - env.close(); - - // Recovery path: existing share holders can still redeem shares - // for the underlying asset via VaultWithdraw. - env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = shares(1)})); - env.close(); - } - { testcase("MPT locked: vault shares inherit underlying lock"); @@ -2458,56 +2315,6 @@ class Vault_test : public beast::unit_test::Suite BEAST_EXPECT(expectOffers(env, alice, 1)); } - { - testcase("MPT non-transferable: pre-fixCleanup3_2_0 share transfer succeeds"); - - // Regression: before fixCleanup3_2_0 a peer-to-peer share Payment - // succeeded even when the underlying asset's lsfMPTCanTransfer - // was cleared. Verify that the legacy (non-inheriting) behavior - // is preserved when the amendment is disabled. - Env env{*this, testableAmendments() - fixCleanup3_2_0}; - Account const issuer{"issuer"}; - Account const owner{"owner"}; - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10'000), issuer, owner, alice, bob); - env.close(); - Vault const vault{env}; - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - PrettyAsset const asset = mptt.issuanceID(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = alice}); - mptt.authorize({.account = bob}); - env(pay(issuer, alice, asset(1'000))); - env(pay(issuer, bob, asset(1'000))); - env.close(); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)})); - env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)})); - env.close(); - - auto const shares = [&]() -> PrettyAsset { - auto const sle = env.le(keylet); - BEAST_EXPECT(sle != nullptr); - return MPTIssue(sle->at(sfShareMPTID)); - }(); - - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Pre-amendment: share transfer leaks past underlying restriction. - env(pay(alice, bob, shares(1))); - env.close(); - } - { testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM"); @@ -2522,8 +2329,8 @@ class Vault_test : public beast::unit_test::Suite MPTTester mptt{env, issuer, kMptInitNoFund}; mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateCanTrade}); + {.flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCanEnableCanTrade}); PrettyAsset const asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = alice}); @@ -2547,38 +2354,18 @@ class Vault_test : public beast::unit_test::Suite return MPTIssue(sle->at(sfShareMPTID)); }(); - // Sanity: while CanTrade is set on the underlying, both the asset - // and the vault share can be placed on the DEX. - env(offer(alice, XRP(1), asset(10))); - env(offer(alice, XRP(1), shares(1))); - env.close(); - - // Issuer governance: clear CanTrade on the underlying. - mptt.set({.mutableFlags = tmfMPTClearCanTrade}); - env.close(); - - // Control: clearing CanTrade on the underlying is observable on - // the DEX path for that asset. + // CanTrade is not set on the underlying, both the asset and + // the vault share are blocked on the DEX. env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); + env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION}); env.close(); - // Control: clearing CanTrade on the underlying is also observable - // on the AMM path for that asset. - AMM const ammUnderlyingFails( + // The inherited CanTrade restriction also blocks AMM creation. + AMM const ammUnderlyingFail( env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION}); - - // Post-fixCleanup3_2_0: vault shares inherit the underlying's - // CanTrade restriction on the DEX path (canTrade reads the - // share's sfReferenceHolding and dispatches to the underlying). - env(offer(bob, XRP(1), shares(1)), Ter{tecNO_PERMISSION}); - env.close(); - - // checkMPTAllowed mirrors the inheritance for AMM/Offer- - // crossing/Check paths, so a share AMM also cannot be created - // when the underlying CanTrade is cleared. AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION}); - // Deposit still works (canAddHolding does not consult the field). + // Deposit still works before enabling CanTrade. env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)})); env.close(); @@ -2587,9 +2374,19 @@ class Vault_test : public beast::unit_test::Suite env(pay(alice, bob, shares(1))); env.close(); - // Withdraw still works. + // Withdraw still works before enabling CanTrade. env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)})); env.close(); + + // Enable CanTrade on the underlying. + mptt.set({.mutableFlags = tmfMPTSetCanTrade}); + env.close(); + + env(offer(alice, XRP(1), asset(10))); + env(offer(alice, XRP(1), shares(1))); + env.close(); + + AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000)); } { diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 1e127e7c05..7da3305eec 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,21 @@ namespace xrpl::test::jtx { +struct MPTSetFlagMapping +{ + std::uint32_t setFlag; + std::uint32_t ledgerFlag; +}; + +static constexpr std::array mptSetFlagMappings = {{ + {.setFlag = tmfMPTSetCanLock, .ledgerFlag = lsfMPTCanLock}, + {.setFlag = tmfMPTSetRequireAuth, .ledgerFlag = lsfMPTRequireAuth}, + {.setFlag = tmfMPTSetCanEscrow, .ledgerFlag = lsfMPTCanEscrow}, + {.setFlag = tmfMPTSetCanClawback, .ledgerFlag = lsfMPTCanClawback}, + {.setFlag = tmfMPTSetCanTrade, .ledgerFlag = lsfMPTCanTrade}, + {.setFlag = tmfMPTSetCanTransfer, .ledgerFlag = lsfMPTCanTransfer}, +}}; + void MptFlags::operator()(Env& env) const { @@ -424,58 +440,12 @@ MPTTester::set(MPTSet const& arg) if (arg.mutableFlags) { - if (*arg.mutableFlags & tmfMPTSetCanLock) + for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings) { - flags |= lsfMPTCanLock; - } - else if (*arg.mutableFlags & tmfMPTClearCanLock) - { - flags &= ~lsfMPTCanLock; - } - - if (*arg.mutableFlags & tmfMPTSetRequireAuth) - { - flags |= lsfMPTRequireAuth; - } - else if (*arg.mutableFlags & tmfMPTClearRequireAuth) - { - flags &= ~lsfMPTRequireAuth; - } - - if (*arg.mutableFlags & tmfMPTSetCanEscrow) - { - flags |= lsfMPTCanEscrow; - } - else if (*arg.mutableFlags & tmfMPTClearCanEscrow) - { - flags &= ~lsfMPTCanEscrow; - } - - if (*arg.mutableFlags & tmfMPTSetCanClawback) - { - flags |= lsfMPTCanClawback; - } - else if (*arg.mutableFlags & tmfMPTClearCanClawback) - { - flags &= ~lsfMPTCanClawback; - } - - if (*arg.mutableFlags & tmfMPTSetCanTrade) - { - flags |= lsfMPTCanTrade; - } - else if (*arg.mutableFlags & tmfMPTClearCanTrade) - { - flags &= ~lsfMPTCanTrade; - } - - if (*arg.mutableFlags & tmfMPTSetCanTransfer) - { - flags |= lsfMPTCanTransfer; - } - else if (*arg.mutableFlags & tmfMPTClearCanTransfer) - { - flags &= ~lsfMPTCanTransfer; + if ((*arg.mutableFlags & setFlag) != 0u) + { + flags |= ledgerFlag; + } } } } diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp index 5593e1b78c..10dd7bacc0 100644 --- a/src/test/jtx/impl/sponsor.cpp +++ b/src/test/jtx/impl/sponsor.cpp @@ -29,7 +29,7 @@ set(jtx::Account const& account, jv[jss::Account] = account.human(); jv[sfFlags.jsonName] = flags; if (reserveCount) - jv[sfReserveCount.jsonName] = *reserveCount; + jv[sfRemainingOwnerCount.jsonName] = *reserveCount; if (feeAmount) jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::Values::None); if (maxFee) @@ -61,7 +61,7 @@ set_reserve(jtx::Account const& account, uint32_t flags, uint32_t reserveCount) jv[jss::TransactionType] = jss::SponsorshipSet; jv[jss::Account] = account.human(); jv[sfFlags.jsonName] = flags; - jv[sfReserveCount.jsonName] = reserveCount; + jv[sfRemainingOwnerCount.jsonName] = reserveCount; return jv; } diff --git a/src/test/nodestore/import_test.cpp b/src/test/nodestore/import_test.cpp index de99edd655..d8c4a96713 100644 --- a/src/test/nodestore/import_test.cpp +++ b/src/test/nodestore/import_test.cpp @@ -21,11 +21,16 @@ #include #include #include + +#if XRPL_ROCKSDB_AVAILABLE + #include #include #include #include +#endif + #include #include #include diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index 961e1b7eb4..e579989181 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -100,6 +100,17 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite return lastSentMessage_; } + // Synchronous test access to the JobQueue-dispatched processor. + // The production path runs this on JtLedgerReq; tests need a + // synchronous entry point to inspect the reply via send(). + // PeerImp::processGetObjectByHash is `protected` so the derived + // test subclass can call it directly. + void + runProcessGetObjectByHash(std::shared_ptr const& m) + { + processGetObjectByHash(m); + } + static void resetId() { @@ -179,6 +190,10 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite /** * Test that reply is limited to hardMaxReplyNodes when more objects * are requested than the limit allows. + * + * `onMessage(TMGetObjectByHash)` dispatches the generic-query path + * to the JobQueue, so tests invoke the synchronous processor + * directly via `runProcessGetObjectByHash`. */ void testReplyLimit(size_t const numObjects, int const expectedReplySize) @@ -191,8 +206,7 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite auto peer = createPeer(env); auto request = createRequest(numObjects, env); - // Call the onMessage handler - peer->onMessage(request); + peer->runProcessGetObjectByHash(request); // Verify that a reply was sent auto sentMessage = peer->getLastSentMessage(); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 83a7122b54..c040c0a5f5 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -950,7 +950,7 @@ public: BEAST_EXPECT(sponsorship[sfSponsee.jsonName] == gw.human()); BEAST_EXPECT( sponsorship[sfFlags.jsonName].asUInt() == tfSponsorshipSetRequireSignForFee); - BEAST_EXPECT(sponsorship[sfReserveCount.jsonName].asUInt() == 200); + BEAST_EXPECT(sponsorship[sfRemainingOwnerCount.jsonName].asUInt() == 200); BEAST_EXPECT(sponsorship[sfFeeAmount.jsonName].asUInt() == 100000000); BEAST_EXPECT(sponsorship[sfMaxFee.jsonName].asUInt() == 10); } @@ -1398,17 +1398,17 @@ public: env.close(); // Helper to call account_objects with sponsored filter - auto acctObjsSponsored = [&env]( - AccountID const& acct, - bool sponsored, - std::optional const& type = std::nullopt) { + auto acctObjsSponsored = [](Env& testEnv, + AccountID const& acct, + bool sponsored, + std::optional const& type = std::nullopt) { json::Value params; params[jss::account] = to_string(acct); params[jss::sponsored] = sponsored; if (type) params[jss::type] = *type; params[jss::ledger_index] = "validated"; - return env.rpc("json", "account_objects", to_string(params)); + return testEnv.rpc("json", "account_objects", to_string(params)); }; // Create a sponsorship (alice sponsors bob) @@ -1421,14 +1421,15 @@ public: // sponsored=true should not find any objects for bob (doesn't have any sponsored objects) { - auto const resp = acctObjsSponsored(bob.id(), true); + auto const resp = acctObjsSponsored(env, bob.id(), true); auto const& objs = resp[jss::result][jss::account_objects]; BEAST_EXPECT(objs.size() == 0); } // Now sponsor bob's trust line auto const trustId = keylet::line(bob, gw, usd.currency); - BEAST_EXPECT(env.le(trustId)); + if (!BEAST_EXPECT(env.le(trustId))) + return; env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key), sponsor::As(sponsor1, spfSponsorReserve), @@ -1438,31 +1439,27 @@ public: // Verify trust line has sponsor field { auto const sle = env.le(trustId); + if (!BEAST_EXPECT(sle)) + return; BEAST_EXPECT(sle->isFieldPresent(sfHighSponsor) || sle->isFieldPresent(sfLowSponsor)); } // sponsored=true on bob should include the sponsored trust line { - auto const resp = acctObjsSponsored(bob.id(), true); + auto const resp = acctObjsSponsored(env, bob.id(), true); auto const& objs = resp[jss::result][jss::account_objects]; - bool foundTrustLine = false; - BEAST_EXPECT(objs.size() == 1); - for (auto const& obj : objs) - { - if (obj[sfLedgerEntryType.jsonName] == jss::RippleState) - { - BEAST_EXPECT( - obj.isMember(sfHighSponsor.jsonName) || - obj.isMember(sfLowSponsor.jsonName)); - foundTrustLine = true; - } - } - BEAST_EXPECT(foundTrustLine); + if (!BEAST_EXPECT(objs.size() == 1)) + return; + + auto const& obj = objs[0u]; + BEAST_EXPECT(obj[sfLedgerEntryType.jsonName] == jss::RippleState); + BEAST_EXPECT( + obj.isMember(sfHighSponsor.jsonName) || obj.isMember(sfLowSponsor.jsonName)); } // sponsored=false on bob should NOT include the sponsored trust line { - auto const resp = acctObjsSponsored(bob.id(), false); + auto const resp = acctObjsSponsored(env, bob.id(), false); auto const& objs = resp[jss::result][jss::account_objects]; bool foundSponsoredTrustLine = false; for (auto const& obj : objs) @@ -1476,38 +1473,105 @@ public: BEAST_EXPECT(!foundSponsoredTrustLine); } - // NFT page sponsored filter + // Only the queried side of a shared trust line should determine + // sponsorship classification. { - // Mint an NFT for bob (creates NFT page) - env(token::mint(bob, 0)); + Env env(*this, testableAmendments()); + Account const issuer("issuer"); + Account const user("user"); + Account const sponsor("sponsor"); + auto const usd = issuer["USD"]; + + env.fund(XRP(10000), issuer, user, sponsor); env.close(); - auto const nftPageKeylet = keylet::nftpageMax(bob); - BEAST_EXPECT(env.le(nftPageKeylet)); - - // Sponsor the NFT page - env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key), - sponsor::As(sponsor1, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor1)); + env(trust(issuer, user["USD"](100))); env.close(); - // Verify NFT page has sponsor field - BEAST_EXPECT(env.le(nftPageKeylet)->isFieldPresent(sfSponsor)); + env(trust(user, usd(100))); + env.close(); + + auto const trustId = keylet::line(user, issuer, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; + + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const line = env.le(trustId); + if (!BEAST_EXPECT(line)) + return; + + auto const userIsHigh = line->getFieldAmount(sfHighLimit).getIssuer() == user.id(); + auto const& userSponsorField = userIsHigh ? sfHighSponsor : sfLowSponsor; + auto const& issuerSponsorField = userIsHigh ? sfLowSponsor : sfHighSponsor; + + BEAST_EXPECT(line->isFieldPresent(userSponsorField)); + BEAST_EXPECT(!line->isFieldPresent(issuerSponsorField)); - // sponsored=true should include the sponsored NFT page - // sponsored=false should NOT include the sponsored NFT page - for (auto const sponsored : {true, false}) { - auto const resp = acctObjsSponsored(bob.id(), sponsored); + auto const resp = acctObjsSponsored(env, user.id(), true, jss::state); auto const& objs = resp[jss::result][jss::account_objects]; - bool foundNFTPage = false; - for (auto const& obj : objs) - { - if (obj[sfLedgerEntryType.jsonName] == jss::NFTokenPage && - obj.isMember(sfSponsor.jsonName)) - foundNFTPage = true; - } - BEAST_EXPECT(foundNFTPage == sponsored); + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + { + auto const resp = acctObjsSponsored(env, user.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + } + + // A Sponsorship object is visible to both sides. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const sponsee("sponsee"); + + env.fund(XRP(10000), owner, sponsee); + env.close(); + + env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee)); + env.close(); + + auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee); + if (!BEAST_EXPECT(env.le(sponsorshipKeylet))) + return; + + { + auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); } } } diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 8f41bb230e..9310bf384a 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -933,15 +933,15 @@ class AccountTx_test : public beast::unit_test::Suite checkTx(alice, jss::SponsorshipSet); checkTx(sponsor, jss::SponsorshipSet); - // create a ticket with sponsor - auto const seq = env.seq(alice); - env(ticket::create(alice, 1), sponsor::As(sponsor, spfSponsorReserve)); + // create an object with sponsor + auto const checkId = keylet::check(alice, env.seq(alice)).key; + env(check::create(alice, sponsor, XRP(1)), sponsor::As(sponsor, spfSponsorReserve)); env.close(); - checkTx(alice, jss::TicketCreate); - checkTx(sponsor, jss::TicketCreate); + checkTx(alice, jss::CheckCreate); + checkTx(sponsor, jss::CheckCreate); // transfer object sponsorship - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::TicketT()(alice, seq + 1).key), + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), sponsor::As(sponsor2, spfSponsorReserve), Sig(sfSponsorSignature, sponsor2)); env.close(); @@ -949,15 +949,14 @@ class AccountTx_test : public beast::unit_test::Suite checkTx(sponsor, jss::SponsorshipTransfer); checkTx(sponsor2, jss::SponsorshipTransfer); - // use a ticket - env(noop(alice), - ticket::Use(seq + 1), + // delete the sponsored object + env(check::cancel(alice, checkId), sponsor::As(sponsor, spfSponsorFee), Sig(sfSponsorSignature, sponsor)); env.close(); - checkTx(alice, jss::AccountSet); - checkTx(sponsor, jss::AccountSet); - checkTx(sponsor2, jss::AccountSet); + checkTx(alice, jss::CheckCancel); + checkTx(sponsor, jss::CheckCancel); + checkTx(sponsor2, jss::CheckCancel); // account sponsorship env(sponsor::transfer(alice, tfSponsorshipCreate), diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index dd9eb1c119..69f1e28170 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -93,6 +94,8 @@ std::vector> gMappings{ {jss::oracle_document_id, FieldType::UInt32Field}, {jss::owner, FieldType::AccountField}, {jss::seq, FieldType::UInt32Field}, + {jss::sponsor, FieldType::AccountField}, + {jss::sponsee, FieldType::AccountField}, {jss::subject, FieldType::AccountField}, {jss::ticket_seq, FieldType::UInt32Field}, }; @@ -107,7 +110,7 @@ getFieldType(json::StaticString fieldName) return it->second; } - Throw("`mappings` is missing field " + std::string(fieldName.cStr())); + Throw("`gMappings` is missing field " + std::string(fieldName.cStr())); } std::string @@ -1886,6 +1889,59 @@ class LedgerEntry_test : public beast::unit_test::Suite runLedgerEntryTest(env, jss::signer_list); } + void + testSponsorship() + { + testcase("Sponsorship"); + + using namespace test::jtx; + + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + env(sponsor::set(alice, 0), sponsor::SponseeAcc(bob)); + env.close(); + std::string const ledgerHash{to_string(env.closed()->header().hash)}; + auto const sponsorshipIndex = to_string(keylet::sponsorship(alice.id(), bob.id()).key); + + { + // Request by sponsor and sponsee. + json::Value jvParams; + jvParams[jss::sponsorship][jss::sponsor] = alice.human(); + jvParams[jss::sponsorship][jss::sponsee] = bob.human(); + jvParams[jss::ledger_hash] = ledgerHash; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human()); + BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString()); + } + { + // Request by index. + json::Value jvParams; + jvParams[jss::sponsorship] = sponsorshipIndex; + jvParams[jss::ledger_hash] = ledgerHash; + json::Value const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship); + BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human()); + BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString()); + } + { + // Check all malformed cases. + runLedgerEntryTest( + env, + jss::sponsorship, + { + {.fieldName = jss::sponsor, .malformedErrorMsg = "malformedSponsor"}, + {.fieldName = jss::sponsee, .malformedErrorMsg = "malformedSponsee"}, + }); + } + } + void testTicket() { @@ -2678,6 +2734,7 @@ public: testPayChan(); testRippleState(); testSignerList(); + testSponsorship(); testTicket(); testDID(); testInvalidOracleLedgerEntry(); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index ed090245a3..0da51872df 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -364,6 +364,20 @@ class Simulate_test : public beast::unit_test::Suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers[0]'."); } + { + // Non-object SponsorSignature field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + txJson[sfSponsorSignature] = ""; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'SponsorSignature', not object."); + } { // Invalid transaction json::Value params; @@ -817,6 +831,58 @@ class Simulate_test : public beast::unit_test::Suite } } + void + testSuccessfulSponsoredTransactionMultisigned() + { + testcase("Successful sponsored multi-signed transaction"); + + using namespace jtx; + Env env(*this); + Account const sponsor("sponsor"); + Account const signer("signer"); + env.fund(XRP(10000), sponsor, signer); + env.close(); + + env(signers(sponsor, 1, {{signer, 1}})); + env.close(); + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto const result = resp[jss::result]; + // Verifies Fee autofill counts nested sponsor-signature signers. + auto const expectedFee = env.current()->fees().base * 2; + checkBasicReturnValidity(result, tx, env.seq(env.master), expectedFee); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain] = "123ABC"; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + tx[sfSponsorSignature.jsonName][sfSigners.jsonName] = json::ValueType::Array; + + json::Value signerObj; + signerObj[sfSigner][jss::Account] = signer.human(); + tx[sfSponsorSignature.jsonName][sfSigners.jsonName].append(signerObj); + + // Leave Fee unset so simulate must autofill it after sponsor signer normalization. + BEAST_EXPECT(!tx.isMember(jss::Fee)); + testTx(env, tx, validateOutput, false); + } + void testTransactionSigningFailure() { @@ -1249,6 +1315,7 @@ public: testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); + testSuccessfulSponsoredTransactionMultisigned(); testTransactionSigningFailure(); testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp index 4da3c83b07..17c80899f9 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/AccountRootTests.cpp @@ -40,12 +40,12 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) auto const mintedNFTokensValue = canonical_UINT32(); auto const burnedNFTokensValue = canonical_UINT32(); auto const firstNFTokenSequenceValue = canonical_UINT32(); - auto const aMMIDValue = canonical_UINT256(); - auto const vaultIDValue = canonical_UINT256(); - auto const loanBrokerIDValue = canonical_UINT256(); auto const sponsoredOwnerCountValue = canonical_UINT32(); auto const sponsoringOwnerCountValue = canonical_UINT32(); auto const sponsoringAccountCountValue = canonical_UINT32(); + auto const aMMIDValue = canonical_UINT256(); + auto const vaultIDValue = canonical_UINT256(); + auto const loanBrokerIDValue = canonical_UINT256(); AccountRootBuilder builder{ accountValue, @@ -70,12 +70,12 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) builder.setMintedNFTokens(mintedNFTokensValue); builder.setBurnedNFTokens(burnedNFTokensValue); builder.setFirstNFTokenSequence(firstNFTokenSequenceValue); - builder.setAMMID(aMMIDValue); - builder.setVaultID(vaultIDValue); - builder.setLoanBrokerID(loanBrokerIDValue); builder.setSponsoredOwnerCount(sponsoredOwnerCountValue); builder.setSponsoringOwnerCount(sponsoringOwnerCountValue); builder.setSponsoringAccountCount(sponsoringAccountCountValue); + builder.setAMMID(aMMIDValue); + builder.setVaultID(vaultIDValue); + builder.setLoanBrokerID(loanBrokerIDValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -234,30 +234,6 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasFirstNFTokenSequence()); } - { - auto const& expected = aMMIDValue; - auto const actualOpt = entry.getAMMID(); - ASSERT_TRUE(actualOpt.has_value()); - expectEqualField(expected, *actualOpt, "sfAMMID"); - EXPECT_TRUE(entry.hasAMMID()); - } - - { - auto const& expected = vaultIDValue; - auto const actualOpt = entry.getVaultID(); - ASSERT_TRUE(actualOpt.has_value()); - expectEqualField(expected, *actualOpt, "sfVaultID"); - EXPECT_TRUE(entry.hasVaultID()); - } - - { - auto const& expected = loanBrokerIDValue; - auto const actualOpt = entry.getLoanBrokerID(); - ASSERT_TRUE(actualOpt.has_value()); - expectEqualField(expected, *actualOpt, "sfLoanBrokerID"); - EXPECT_TRUE(entry.hasLoanBrokerID()); - } - { auto const& expected = sponsoredOwnerCountValue; auto const actualOpt = entry.getSponsoredOwnerCount(); @@ -282,6 +258,30 @@ TEST(AccountRootTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasSponsoringAccountCount()); } + { + auto const& expected = aMMIDValue; + auto const actualOpt = entry.getAMMID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAMMID"); + EXPECT_TRUE(entry.hasAMMID()); + } + + { + auto const& expected = vaultIDValue; + auto const actualOpt = entry.getVaultID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfVaultID"); + EXPECT_TRUE(entry.hasVaultID()); + } + + { + auto const& expected = loanBrokerIDValue; + auto const actualOpt = entry.getLoanBrokerID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfLoanBrokerID"); + EXPECT_TRUE(entry.hasLoanBrokerID()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -315,12 +315,12 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) auto const mintedNFTokensValue = canonical_UINT32(); auto const burnedNFTokensValue = canonical_UINT32(); auto const firstNFTokenSequenceValue = canonical_UINT32(); - auto const aMMIDValue = canonical_UINT256(); - auto const vaultIDValue = canonical_UINT256(); - auto const loanBrokerIDValue = canonical_UINT256(); auto const sponsoredOwnerCountValue = canonical_UINT32(); auto const sponsoringOwnerCountValue = canonical_UINT32(); auto const sponsoringAccountCountValue = canonical_UINT32(); + auto const aMMIDValue = canonical_UINT256(); + auto const vaultIDValue = canonical_UINT256(); + auto const loanBrokerIDValue = canonical_UINT256(); auto sle = std::make_shared(AccountRoot::entryType, index); @@ -344,12 +344,12 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) sle->at(sfMintedNFTokens) = mintedNFTokensValue; sle->at(sfBurnedNFTokens) = burnedNFTokensValue; sle->at(sfFirstNFTokenSequence) = firstNFTokenSequenceValue; - sle->at(sfAMMID) = aMMIDValue; - sle->at(sfVaultID) = vaultIDValue; - sle->at(sfLoanBrokerID) = loanBrokerIDValue; sle->at(sfSponsoredOwnerCount) = sponsoredOwnerCountValue; sle->at(sfSponsoringOwnerCount) = sponsoringOwnerCountValue; sle->at(sfSponsoringAccountCount) = sponsoringAccountCountValue; + sle->at(sfAMMID) = aMMIDValue; + sle->at(sfVaultID) = vaultIDValue; + sle->at(sfLoanBrokerID) = loanBrokerIDValue; AccountRootBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -602,45 +602,6 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfFirstNFTokenSequence"); } - { - auto const& expected = aMMIDValue; - - auto const fromSleOpt = entryFromSle.getAMMID(); - auto const fromBuilderOpt = entryFromBuilder.getAMMID(); - - ASSERT_TRUE(fromSleOpt.has_value()); - ASSERT_TRUE(fromBuilderOpt.has_value()); - - expectEqualField(expected, *fromSleOpt, "sfAMMID"); - expectEqualField(expected, *fromBuilderOpt, "sfAMMID"); - } - - { - auto const& expected = vaultIDValue; - - auto const fromSleOpt = entryFromSle.getVaultID(); - auto const fromBuilderOpt = entryFromBuilder.getVaultID(); - - ASSERT_TRUE(fromSleOpt.has_value()); - ASSERT_TRUE(fromBuilderOpt.has_value()); - - expectEqualField(expected, *fromSleOpt, "sfVaultID"); - expectEqualField(expected, *fromBuilderOpt, "sfVaultID"); - } - - { - auto const& expected = loanBrokerIDValue; - - auto const fromSleOpt = entryFromSle.getLoanBrokerID(); - auto const fromBuilderOpt = entryFromBuilder.getLoanBrokerID(); - - ASSERT_TRUE(fromSleOpt.has_value()); - ASSERT_TRUE(fromBuilderOpt.has_value()); - - expectEqualField(expected, *fromSleOpt, "sfLoanBrokerID"); - expectEqualField(expected, *fromBuilderOpt, "sfLoanBrokerID"); - } - { auto const& expected = sponsoredOwnerCountValue; @@ -680,6 +641,45 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfSponsoringAccountCount"); } + { + auto const& expected = aMMIDValue; + + auto const fromSleOpt = entryFromSle.getAMMID(); + auto const fromBuilderOpt = entryFromBuilder.getAMMID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAMMID"); + expectEqualField(expected, *fromBuilderOpt, "sfAMMID"); + } + + { + auto const& expected = vaultIDValue; + + auto const fromSleOpt = entryFromSle.getVaultID(); + auto const fromBuilderOpt = entryFromBuilder.getVaultID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfVaultID"); + expectEqualField(expected, *fromBuilderOpt, "sfVaultID"); + } + + { + auto const& expected = loanBrokerIDValue; + + auto const fromSleOpt = entryFromSle.getLoanBrokerID(); + auto const fromBuilderOpt = entryFromBuilder.getLoanBrokerID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfLoanBrokerID"); + expectEqualField(expected, *fromBuilderOpt, "sfLoanBrokerID"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -772,17 +772,17 @@ TEST(AccountRootTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getBurnedNFTokens().has_value()); EXPECT_FALSE(entry.hasFirstNFTokenSequence()); EXPECT_FALSE(entry.getFirstNFTokenSequence().has_value()); - EXPECT_FALSE(entry.hasAMMID()); - EXPECT_FALSE(entry.getAMMID().has_value()); - EXPECT_FALSE(entry.hasVaultID()); - EXPECT_FALSE(entry.getVaultID().has_value()); - EXPECT_FALSE(entry.hasLoanBrokerID()); - EXPECT_FALSE(entry.getLoanBrokerID().has_value()); EXPECT_FALSE(entry.hasSponsoredOwnerCount()); EXPECT_FALSE(entry.getSponsoredOwnerCount().has_value()); EXPECT_FALSE(entry.hasSponsoringOwnerCount()); EXPECT_FALSE(entry.getSponsoringOwnerCount().has_value()); EXPECT_FALSE(entry.hasSponsoringAccountCount()); EXPECT_FALSE(entry.getSponsoringAccountCount().has_value()); + EXPECT_FALSE(entry.hasAMMID()); + EXPECT_FALSE(entry.getAMMID().has_value()); + EXPECT_FALSE(entry.hasVaultID()); + EXPECT_FALSE(entry.getVaultID().has_value()); + EXPECT_FALSE(entry.hasLoanBrokerID()); + EXPECT_FALSE(entry.getLoanBrokerID().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp index 5e805164f4..e1d9ff15b9 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/SponsorshipTests.cpp @@ -26,7 +26,7 @@ TEST(SponsorshipTests, BuilderSettersRoundTrip) auto const sponseeValue = canonical_ACCOUNT(); auto const feeAmountValue = canonical_AMOUNT(); auto const maxFeeValue = canonical_AMOUNT(); - auto const reserveCountValue = canonical_UINT32(); + auto const remainingOwnerCountValue = canonical_UINT32(); auto const ownerNodeValue = canonical_UINT64(); auto const sponseeNodeValue = canonical_UINT64(); @@ -41,7 +41,7 @@ TEST(SponsorshipTests, BuilderSettersRoundTrip) builder.setFeeAmount(feeAmountValue); builder.setMaxFee(maxFeeValue); - builder.setReserveCount(reserveCountValue); + builder.setRemainingOwnerCount(remainingOwnerCountValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -105,11 +105,11 @@ TEST(SponsorshipTests, BuilderSettersRoundTrip) } { - auto const& expected = reserveCountValue; - auto const actualOpt = entry.getReserveCount(); + auto const& expected = remainingOwnerCountValue; + auto const actualOpt = entry.getRemainingOwnerCount(); ASSERT_TRUE(actualOpt.has_value()); - expectEqualField(expected, *actualOpt, "sfReserveCount"); - EXPECT_TRUE(entry.hasReserveCount()); + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); + EXPECT_TRUE(entry.hasRemainingOwnerCount()); } EXPECT_TRUE(entry.hasLedgerIndex()); @@ -131,7 +131,7 @@ TEST(SponsorshipTests, BuilderFromSleRoundTrip) auto const sponseeValue = canonical_ACCOUNT(); auto const feeAmountValue = canonical_AMOUNT(); auto const maxFeeValue = canonical_AMOUNT(); - auto const reserveCountValue = canonical_UINT32(); + auto const remainingOwnerCountValue = canonical_UINT32(); auto const ownerNodeValue = canonical_UINT64(); auto const sponseeNodeValue = canonical_UINT64(); @@ -143,7 +143,7 @@ TEST(SponsorshipTests, BuilderFromSleRoundTrip) sle->at(sfSponsee) = sponseeValue; sle->at(sfFeeAmount) = feeAmountValue; sle->at(sfMaxFee) = maxFeeValue; - sle->at(sfReserveCount) = reserveCountValue; + sle->at(sfRemainingOwnerCount) = remainingOwnerCountValue; sle->at(sfOwnerNode) = ownerNodeValue; sle->at(sfSponseeNode) = sponseeNodeValue; @@ -243,16 +243,16 @@ TEST(SponsorshipTests, BuilderFromSleRoundTrip) } { - auto const& expected = reserveCountValue; + auto const& expected = remainingOwnerCountValue; - auto const fromSleOpt = entryFromSle.getReserveCount(); - auto const fromBuilderOpt = entryFromBuilder.getReserveCount(); + auto const fromSleOpt = entryFromSle.getRemainingOwnerCount(); + auto const fromBuilderOpt = entryFromBuilder.getRemainingOwnerCount(); ASSERT_TRUE(fromSleOpt.has_value()); ASSERT_TRUE(fromBuilderOpt.has_value()); - expectEqualField(expected, *fromSleOpt, "sfReserveCount"); - expectEqualField(expected, *fromBuilderOpt, "sfReserveCount"); + expectEqualField(expected, *fromSleOpt, "sfRemainingOwnerCount"); + expectEqualField(expected, *fromBuilderOpt, "sfRemainingOwnerCount"); } EXPECT_EQ(entryFromSle.getKey(), index); @@ -323,7 +323,7 @@ TEST(SponsorshipTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getFeeAmount().has_value()); EXPECT_FALSE(entry.hasMaxFee()); EXPECT_FALSE(entry.getMaxFee().has_value()); - EXPECT_FALSE(entry.hasReserveCount()); - EXPECT_FALSE(entry.getReserveCount().has_value()); + EXPECT_FALSE(entry.hasRemainingOwnerCount()); + EXPECT_FALSE(entry.getRemainingOwnerCount().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp index 3a19038714..dce8cfca3f 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp @@ -33,7 +33,7 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) auto const sponseeValue = canonical_ACCOUNT(); auto const feeAmountValue = canonical_AMOUNT(); auto const maxFeeValue = canonical_AMOUNT(); - auto const reserveCountValue = canonical_UINT32(); + auto const remainingOwnerCountValue = canonical_UINT32(); SponsorshipSetBuilder builder{ accountValue, @@ -46,7 +46,7 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) builder.setSponsee(sponseeValue); builder.setFeeAmount(feeAmountValue); builder.setMaxFee(maxFeeValue); - builder.setReserveCount(reserveCountValue); + builder.setRemainingOwnerCount(remainingOwnerCountValue); auto tx = builder.build(publicKey, secretKey); @@ -97,11 +97,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) } { - auto const& expected = reserveCountValue; - auto const actualOpt = tx.getReserveCount(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfReserveCount should be present"; - expectEqualField(expected, *actualOpt, "sfReserveCount"); - EXPECT_TRUE(tx.hasReserveCount()); + auto const& expected = remainingOwnerCountValue; + auto const actualOpt = tx.getRemainingOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present"; + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); + EXPECT_TRUE(tx.hasRemainingOwnerCount()); } } @@ -124,7 +124,7 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) auto const sponseeValue = canonical_ACCOUNT(); auto const feeAmountValue = canonical_AMOUNT(); auto const maxFeeValue = canonical_AMOUNT(); - auto const reserveCountValue = canonical_UINT32(); + auto const remainingOwnerCountValue = canonical_UINT32(); // Build an initial transaction SponsorshipSetBuilder initialBuilder{ @@ -137,7 +137,7 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) initialBuilder.setSponsee(sponseeValue); initialBuilder.setFeeAmount(feeAmountValue); initialBuilder.setMaxFee(maxFeeValue); - initialBuilder.setReserveCount(reserveCountValue); + initialBuilder.setRemainingOwnerCount(remainingOwnerCountValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -185,10 +185,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) } { - auto const& expected = reserveCountValue; - auto const actualOpt = rebuiltTx.getReserveCount(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfReserveCount should be present"; - expectEqualField(expected, *actualOpt, "sfReserveCount"); + auto const& expected = remainingOwnerCountValue; + auto const actualOpt = rebuiltTx.getRemainingOwnerCount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present"; + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); } } @@ -254,8 +254,8 @@ TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getFeeAmount().has_value()); EXPECT_FALSE(tx.hasMaxFee()); EXPECT_FALSE(tx.getMaxFee().has_value()); - EXPECT_FALSE(tx.hasReserveCount()); - EXPECT_FALSE(tx.getReserveCount().has_value()); + EXPECT_FALSE(tx.hasRemainingOwnerCount()); + EXPECT_FALSE(tx.getRemainingOwnerCount().has_value()); } } diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.cpp b/src/xrpld/app/ledger/OrderBookDBImpl.cpp index 658ec4ea7a..1e474ef949 100644 --- a/src/xrpld/app/ledger/OrderBookDBImpl.cpp +++ b/src/xrpld/app/ledger/OrderBookDBImpl.cpp @@ -9,19 +9,17 @@ #include #include #include -#include #include +#include #include #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -307,55 +305,10 @@ OrderBookDBImpl::isBookToXRP(Asset const& asset, std::optional const& do return xrpBooks_.contains(asset); } -BookListeners::pointer -OrderBookDBImpl::makeBookListeners(Book const& book) +hash_set +affectedBooks(AcceptedLedgerTx const& alTx, beast::Journal const& j) { - std::scoped_lock const sl(lock_); - auto ret = getBookListeners(book); - - if (!ret) - { - ret = std::make_shared(); - - listeners_[book] = ret; - XRPL_ASSERT( - getBookListeners(book) == ret, - "xrpl::OrderBookDB::makeBookListeners : result roundtrip " - "lookup"); - } - - return ret; -} - -BookListeners::pointer -OrderBookDBImpl::getBookListeners(Book const& book) -{ - BookListeners::pointer ret; - std::scoped_lock const sl(lock_); - - auto it0 = listeners_.find(book); - if (it0 != listeners_.end()) - ret = it0->second; - - return ret; -} - -// Based on the meta, send the meta to the streams that are listening. -// We need to determine which streams a given meta effects. -void -OrderBookDBImpl::processTxn( - std::shared_ptr const& ledger, - AcceptedLedgerTx const& alTx, - MultiApiJson const& jvObj) -{ - std::scoped_lock const sl(lock_); - - // For this particular transaction, maintain the set of unique - // subscriptions that have already published it. This prevents sending - // the transaction multiple times if it touches multiple ltOFFER - // entries for the same book, or if it touches multiple books and a - // single client has subscribed to those books. - hash_set havePublished; + hash_set result; for (auto const& node : alTx.getMeta().getNodes()) { @@ -363,40 +316,41 @@ OrderBookDBImpl::processTxn( { if (node.getFieldU16(sfLedgerEntryType) == ltOFFER) { - auto process = [&, this](SField const& field) { + auto extract = [&](SField const& field) { if (auto data = dynamic_cast(node.peekAtPField(field)); data && data->isFieldPresent(sfTakerPays) && data->isFieldPresent(sfTakerGets)) { - auto listeners = getBookListeners( - {data->getFieldAmount(sfTakerGets).asset(), - data->getFieldAmount(sfTakerPays).asset(), - (*data)[~sfDomainID]}); - if (listeners) - listeners->publish(jvObj, havePublished); + result.emplace( + data->getFieldAmount(sfTakerGets).asset(), + data->getFieldAmount(sfTakerPays).asset(), + (*data)[~sfDomainID]); } }; - // We need a field that contains the TakerGets and TakerPays - // parameters. if (node.getFName() == sfModifiedNode) { - process(sfPreviousFields); + extract(sfPreviousFields); } else if (node.getFName() == sfCreatedNode) { - process(sfNewFields); + extract(sfNewFields); } else if (node.getFName() == sfDeletedNode) { - process(sfFinalFields); + extract(sfFinalFields); } } } catch (std::exception const& ex) { - JLOG(j_.info()) << "processTxn: field not found (" << ex.what() << ")"; + // The bad node is skipped; other affected books in the same + // transaction are still returned. Logged at warn so a malformed + // offer node is visible to operators. + JLOG(j.warn()) << "affectedBooks: skipping malformed node (" << ex.what() << ")"; } } + + return result; } } // namespace xrpl diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.h b/src/xrpld/app/ledger/OrderBookDBImpl.h index a50f512441..a68f63c043 100644 --- a/src/xrpld/app/ledger/OrderBookDBImpl.h +++ b/src/xrpld/app/ledger/OrderBookDBImpl.h @@ -1,10 +1,7 @@ #pragma once #include -#include -#include #include -#include #include #include @@ -54,18 +51,6 @@ public: void update(std::shared_ptr const& ledger); - // see if this txn effects any orderbook - void - processTxn( - std::shared_ptr const& ledger, - AcceptedLedgerTx const& alTx, - MultiApiJson const& jvObj) override; - - BookListeners::pointer - getBookListeners(Book const&) override; - BookListeners::pointer - makeBookListeners(Book const&) override; - private: std::reference_wrapper registry_; int const pathSearchMax_; @@ -84,10 +69,6 @@ private: std::recursive_mutex lock_; - using BookToListenersMap = hash_map; - - BookToListenersMap listeners_; - std::atomic seq_; beast::Journal const j_; diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 917cf6aeb2..d807dea10a 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -527,6 +527,8 @@ public: updateLocalTx(ReadView const& view) override; std::size_t getLocalTxCount() override; + std::size_t + getBookSubscribersCount() override; // // Monitoring: publisher side. @@ -586,7 +588,9 @@ public: bool subBook(InfoSub::ref ispListener, Book const&) override; bool - unsubBook(std::uint64_t uListener, Book const&) override; + unsubBook(InfoSub::ref ispListener, Book const&) override; + bool + unsubBookInternal(std::uint64_t uListener, Book const&) override; bool subManifests(InfoSub::ref ispListener) override; @@ -629,6 +633,12 @@ public: bool tryRemoveRpcSub(std::string const& strUrl) override; + beast::Journal const& + journal() const override + { + return journal_; + } + void stop() override { @@ -705,6 +715,32 @@ private: AcceptedLedgerTx const& transaction, bool last); + /** + * Fan transaction notifications out to all book subscribers. + * + * Extracts the set of order books affected by @p transaction, then + * delivers @p jvObj to every live subscriber of those books. + * + * Uses a two-pass design to keep subLock_ hold time short: + * 1. Under subLock_, collect strong InfoSub pointers for all live + * subscribers and prune any expired weak_ptrs encountered. + * 2. Release subLock_, then call send() on each collected pointer. + * + * @param transaction The accepted ledger transaction to inspect. + * @param jvObj JSON representation of the transaction to deliver. + * + * @note Thread-safety: acquires subLock_ for the collection pass only. + * send() is intentionally called outside the lock to avoid blocking + * all other sub/unsub/publish paths while I/O is in progress. + * @note Contention: subLock_ is shared with all other subscription types. + * On high-throughput nodes processing multi-hop payments that touch + * many offer nodes, this pass holds subLock_ longer than the old + * per-book BookListeners locks did. This is an accepted trade-off + * for lock-domain simplicity. + */ + void + pubBookTransaction(AcceptedLedgerTx const& transaction, MultiApiJson const& jvObj); + void pubProposedAccountTransaction( std::shared_ptr const& ledger, @@ -802,8 +838,19 @@ private: LedgerMaster& ledgerMaster_; + /** Maps each order book to its current set of subscribers. + * Outer key: the Book (currency pair + optional domain). + * Inner key: InfoSub::seq (unique per connection). + * Inner value: weak_ptr so that a dropped connection does not prevent + * the InfoSub from being destroyed; expired entries are pruned lazily + * by pubBookTransaction and eagerly by unsubBookInternal (~InfoSub path). + * Guarded by subLock_. + */ + using SubBookMapType = hash_map; + SubInfoMapType subAccount_; SubInfoMapType subRTAccount_; + SubBookMapType subBook_; ///< Guarded by subLock_. subRpcMapType rpcSubMap_; @@ -3192,6 +3239,16 @@ NetworkOPsImp::getLocalTxCount() return localTX_->size(); } +std::size_t +NetworkOPsImp::getBookSubscribersCount() +{ + std::scoped_lock const sl(subLock_); + std::size_t total = 0; + for (auto const& [_, subs] : subBook_) + total += subs.size(); + return total; +} + // This routine should only be used to publish accepted or validated // transactions. MultiApiJson @@ -3353,11 +3410,89 @@ NetworkOPsImp::pubValidatedTransaction( } if (transaction.getResult() == tesSUCCESS) - registry_.get().getOrderBookDB().processTxn(ledger, transaction, jvObj); + pubBookTransaction(transaction, jvObj); pubAccountTransaction(ledger, transaction, last); } +void +NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson const& jvObj) +{ + auto const books = affectedBooks(alTx, journal_); + if (books.empty()) + return; + + // Two-pass design: + // + // 1. Under subLock_, walk subBook_, collect a strong pointer for each + // unique listener (and prune any expired weak_ptrs we encounter). + // 2. Release subLock_, then send to each collected listener. + // + // Reasoning: + // * send() can be slow / blocking, so holding subLock_ across it would + // stall every other sub/unsub/pub path on this server (see the matching + // TODO above pubServer at line ~2275). + // * A strong pointer destructed while subLock_ is held risks running + // ~InfoSub() in-line, which re-enters unsubBook() and mutates the very + // subBook_/SubMapType being iterated -> dangling iterator UB. + // + // Releasing subLock_ before any InfoSub::pointer can decay solves both. + // ~InfoSub() reacquires subLock_ via unsubBook() on its own and serializes + // safely with concurrent traffic. + + std::vector listeners; + hash_set seen; + + // Sized for the common case where every affected book has at most + // one subscriber. Multi-subscriber books trigger reallocation, but + // that is rare and the upper-bound estimate (sum of per-book sizes) + // would itself require walking subBook_ twice. + listeners.reserve(books.size()); + seen.reserve(books.size()); + + { + std::scoped_lock const sl(subLock_); + + for (auto const& book : books) + { + auto it = subBook_.find(book); + if (it == subBook_.end()) + continue; + + for (auto sit = it->second.begin(); sit != it->second.end();) + { + if (auto p = sit->second.lock()) + { + // Defensive: subBook_ entries are normally cleared by + // ~InfoSub() -> unsubBook(), so we rarely see expired + // weak_ptrs here. The else branch covers the narrow race + // where the last strong ref is dropped between insertion + // and our lock() call. + if (seen.emplace(p->getSeq()).second) + listeners.emplace_back(std::move(p)); + ++sit; + } + else + { + JLOG(journal_.debug()) + << "pubBookTransaction: pruning expired weak_ptr for seq=" << sit->first; + sit = it->second.erase(sit); + } + } + + if (it->second.empty()) + subBook_.erase(it); + } + } + + for (auto const& p : listeners) + { + jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); }); + } + // listeners destructs here, outside subLock_; ~InfoSub (if any fires) + // will reacquire subLock_ via unsubBook with no iterator hazard. +} + void NetworkOPsImp::pubAccountTransaction( std::shared_ptr const& ledger, @@ -4011,26 +4146,39 @@ NetworkOPsImp::unsubAccountHistoryInternal( bool NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book) { - if (auto listeners = registry_.get().getOrderBookDB().makeBookListeners(book)) + // Server-side insert first, then InfoSub bookkeeping. If the InfoSub-side + // insert throws, the orphan in subBook_ is cleared by the expired-weak_ptr + // prune in pubBookTransaction. With the reverse ordering, ~InfoSub would + // call unsubBookInternal for a key that was never inserted server-side. { - listeners->addSubscriber(isrListener); - } - else - { - // LCOV_EXCL_START - UNREACHABLE("xrpl::NetworkOPsImp::subBook : null book listeners"); - // LCOV_EXCL_STOP + std::scoped_lock const sl(subLock_); + subBook_[book].try_emplace(isrListener->getSeq(), isrListener); } + isrListener->insertBookSubscription(book); return true; } bool -NetworkOPsImp::unsubBook(std::uint64_t uSeq, Book const& book) +NetworkOPsImp::unsubBook(InfoSub::ref isrListener, Book const& book) { - if (auto listeners = registry_.get().getOrderBookDB().getBookListeners(book)) - listeners->removeSubscriber(uSeq); + // Mirrors unsubAccount: clear the per-subscriber tracking set first so + // ~InfoSub does not re-issue an unsubBookInternal for a book the caller + // already removed, then erase the server-side entry. + isrListener->deleteBookSubscription(book); + return unsubBookInternal(isrListener->getSeq(), book); +} - return true; +bool +NetworkOPsImp::unsubBookInternal(std::uint64_t uSeq, Book const& book) +{ + std::scoped_lock const sl(subLock_); + auto it = subBook_.find(book); + if (it == subBook_.end()) + return false; + bool const erased = it->second.erase(uSeq) != 0u; + if (it->second.empty()) + subBook_.erase(it); + return erased; } std::uint32_t diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 0326828a70..b4fb40f368 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -398,6 +399,10 @@ TxQ::canBeHeld( ((flags & TapFailHard) != 0u)) return telCAN_NOT_QUEUE; + // Disallow sponsored transactions from being queued. + if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx)) + return telCAN_NOT_QUEUE; + { // To be queued and relayed, the transaction needs to // promise to stick around for long enough that it has diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index cda576add0..e1d7d23215 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -69,7 +70,6 @@ #include #include #include -#include #include #include #include @@ -81,6 +81,7 @@ #include #include +#include #include #include #include @@ -196,78 +197,70 @@ stringIsUInt256Sized(std::string const& pBuffStr) void PeerImp::run() { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::run, shared_from_this())); - return; - } + dispatch(strand_, [self = shared_from_this()]() { + auto parseLedgerHash = [](std::string_view value) -> std::optional { + if (uint256 ret; ret.parseHex(value)) + return ret; - auto parseLedgerHash = [](std::string_view value) -> std::optional { - if (uint256 ret; ret.parseHex(value)) - return ret; + if (auto const s = base64Decode(value); s.size() == uint256::size()) + return uint256::fromRaw(s); - if (auto const s = base64Decode(value); s.size() == uint256::size()) - return uint256::fromRaw(s); + return std::nullopt; + }; - return std::nullopt; - }; + std::optional closed; + std::optional previous; - std::optional closed; - std::optional previous; + if (auto const iter = self->headers_.find("Closed-Ledger"); iter != self->headers_.end()) + { + closed = parseLedgerHash(iter->value()); - if (auto const iter = headers_.find("Closed-Ledger"); iter != headers_.end()) - { - closed = parseLedgerHash(iter->value()); + if (!closed) + self->fail("Malformed handshake data (1)"); + } - if (!closed) - fail("Malformed handshake data (1)"); - } + if (auto const iter = self->headers_.find("Previous-Ledger"); iter != self->headers_.end()) + { + previous = parseLedgerHash(iter->value()); - if (auto const iter = headers_.find("Previous-Ledger"); iter != headers_.end()) - { - previous = parseLedgerHash(iter->value()); + if (!previous) + self->fail("Malformed handshake data (2)"); + } - if (!previous) - fail("Malformed handshake data (2)"); - } + if (previous && !closed) + self->fail("Malformed handshake data (3)"); - if (previous && !closed) - fail("Malformed handshake data (3)"); + { + std::scoped_lock const sl(self->recentLock_); + if (closed) + self->closedLedgerHash_ = *closed; + if (previous) + self->previousLedgerHash_ = *previous; + } - { - std::scoped_lock const sl(recentLock_); - if (closed) - closedLedgerHash_ = *closed; - if (previous) - previousLedgerHash_ = *previous; - } + if (self->inbound_) + { + self->doAccept(); + } + else + { + self->doProtocolStart(); + } - if (inbound_) - { - doAccept(); - } - else - { - doProtocolStart(); - } - - // Anything else that needs to be done with the connection should be - // done in doProtocolStart + // Anything else that needs to be done with the connection should be + // done in doProtocolStart + }); } void PeerImp::stop() { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::stop, shared_from_this())); - return; - } + dispatch(strand_, [self = shared_from_this()]() { + if (!self->socket_.is_open()) + return; - if (!socket_.is_open()) - return; - - close(); + self->close(); + }); } //------------------------------------------------------------------------------ @@ -275,130 +268,125 @@ PeerImp::stop() void PeerImp::send(std::shared_ptr const& m) { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::send, shared_from_this(), m)); - return; - } - if (gracefulClose_) - return; - if (detaching_) - return; - if (!socket_.is_open()) - return; + dispatch(strand_, [self = shared_from_this(), m]() { + if (self->gracefulClose_) + return; + if (self->detaching_) + return; + if (!self->socket_.is_open()) + return; - auto validator = m->getValidatorKey(); - if (validator && !squelch_.expireSquelch(*validator)) - { - overlay_.reportOutboundTraffic( - TrafficCount::Category::SquelchSuppressed, - static_cast(m->getBuffer(compressionEnabled_).size())); - return; - } + auto validator = m->getValidatorKey(); + if (validator && !self->squelch_.expireSquelch(*validator)) + { + self->overlay_.reportOutboundTraffic( + TrafficCount::Category::SquelchSuppressed, + static_cast(m->getBuffer(self->compressionEnabled_).size())); + return; + } - // report categorized outgoing traffic - overlay_.reportOutboundTraffic( - safeCast(m->getCategory()), - static_cast(m->getBuffer(compressionEnabled_).size())); + // report categorized outgoing traffic + self->overlay_.reportOutboundTraffic( + safeCast(m->getCategory()), + static_cast(m->getBuffer(self->compressionEnabled_).size())); - // report total outgoing traffic - overlay_.reportOutboundTraffic( - TrafficCount::Category::Total, static_cast(m->getBuffer(compressionEnabled_).size())); + // report total outgoing traffic + self->overlay_.reportOutboundTraffic( + TrafficCount::Category::Total, + static_cast(m->getBuffer(self->compressionEnabled_).size())); - auto sendqSize = sendQueue_.size(); + auto sendqSize = self->sendQueue_.size(); - if (sendqSize < Tuning::kTargetSendQueue) - { - // To detect a peer that does not read from their - // side of the connection, we expect a peer to have - // a small senq periodically - largeSendq_ = 0; - } - else if (auto sink = journal_.debug(); sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0) - { - std::string const n = name(); - sink << n << " sendq: " << sendqSize; - } + if (sendqSize < Tuning::kTargetSendQueue) + { + // To detect a peer that does not read from their + // side of the connection, we expect a peer to have + // a small sendq periodically + self->largeSendq_ = 0; + } + else if ( + auto sink = self->journal_.debug(); + sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0) + { + std::string const n = self->name(); + sink << n << " sendq: " << sendqSize; + } - sendQueue_.push(m); + self->sendQueue_.push(m); - if (sendqSize != 0) - return; + if (sendqSize != 0) + return; - boost::asio::async_write( - stream_, - boost::asio::buffer(sendQueue_.front()->getBuffer(compressionEnabled_)), - bind_executor( - strand_, - std::bind( - &PeerImp::onWriteMessage, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + boost::asio::async_write( + self->stream_, + boost::asio::buffer(self->sendQueue_.front()->getBuffer(self->compressionEnabled_)), + bind_executor( + self->strand_, + std::bind( + &PeerImp::onWriteMessage, self, std::placeholders::_1, std::placeholders::_2))); + }); } void PeerImp::sendTxQueue() { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::sendTxQueue, shared_from_this())); - return; - } - - if (!txQueue_.empty()) - { - protocol::TMHaveTransactions ht; - std::ranges::for_each( - txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); }); - JLOG(pJournal_.trace()) << "sendTxQueue " << txQueue_.size(); - txQueue_.clear(); - send(std::make_shared(ht, protocol::mtHAVE_TRANSACTIONS)); - } + dispatch(strand_, [self = shared_from_this()]() { + if (!self->txQueue_.empty()) + { + protocol::TMHaveTransactions ht; + std::ranges::for_each( + self->txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); }); + JLOG(self->pJournal_.trace()) << "sendTxQueue " << self->txQueue_.size(); + self->txQueue_.clear(); + self->send(std::make_shared(ht, protocol::mtHAVE_TRANSACTIONS)); + } + }); } void PeerImp::addTxQueue(uint256 const& hash) { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::addTxQueue, shared_from_this(), hash)); - return; - } + dispatch(strand_, [self = shared_from_this(), hash]() { + if (self->txQueue_.size() == reduce_relay::kMaxTxQueueSize) + { + JLOG(self->pJournal_.warn()) << "addTxQueue exceeds the cap"; + self->sendTxQueue(); + } - if (txQueue_.size() == reduce_relay::kMaxTxQueueSize) - { - JLOG(pJournal_.warn()) << "addTxQueue exceeds the cap"; - sendTxQueue(); - } - - txQueue_.insert(hash); - JLOG(pJournal_.trace()) << "addTxQueue " << txQueue_.size(); + self->txQueue_.insert(hash); + JLOG(self->pJournal_.trace()) << "addTxQueue " << self->txQueue_.size(); + }); } void PeerImp::removeTxQueue(uint256 const& hash) { - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind(&PeerImp::removeTxQueue, shared_from_this(), hash)); - return; - } - - auto removed = txQueue_.erase(hash); - JLOG(pJournal_.trace()) << "removeTxQueue " << removed; + dispatch(strand_, [self = shared_from_this(), hash]() { + auto removed = self->txQueue_.erase(hash); + JLOG(self->pJournal_.trace()) << "removeTxQueue " << removed; + }); } void PeerImp::charge(Resource::Charge const& fee, std::string const& context) { - dispatch(strand_, [this, self = shared_from_this(), fee, context]() { - if (usage_.charge(fee, context) == Resource::Disposition::Drop && - usage_.disconnect(pJournal_)) + dispatch(strand_, [self = shared_from_this(), fee, context]() { + if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) && + self->usage_.disconnect(self->pJournal_)) { - // Sever the connection. - overlay_.incPeerDisconnectCharges(); - fail("charge: Resources"); + // Idempotent: only the first worker to observe Drop counts the + // metric and posts fail(). Without the guard, several queued + // workers can all see Drop before fail() lands on the strand, + // overcounting peerDisconnectsCharges_ and posting duplicate + // shutdowns. fail(std::string const&) self-posts to strand_ + // when invoked off-strand. + bool expected = false; + if (self->chargeDisconnectFired_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) + { + self->overlay_.incPeerDisconnectCharges(); + self->fail("charge: Resources"); + } } }); } @@ -628,20 +616,14 @@ PeerImp::close() void PeerImp::fail(std::string const& reason) { - if (!strand_.running_in_this_thread()) - { - post( - strand_, - std::bind( - (void (Peer::*)(std::string const&))&PeerImp::fail, shared_from_this(), reason)); - return; - } - if (journal_.active(beast::Severity::Warning) && socket_.is_open()) - { - std::string const n = name(); - JLOG(journal_.warn()) << n << " failed: " << reason; - } - close(); + dispatch(strand_, [self = shared_from_this(), reason]() { + if (self->journal_.active(beast::Severity::Warning) && self->socket_.is_open()) + { + std::string const n = self->name(); + JLOG(self->journal_.warn()) << n << " failed: " << reason; + } + self->close(); + }); } void @@ -2034,7 +2016,7 @@ PeerImp::checkTracking(std::uint32_t validationSeq) void PeerImp::checkTracking(std::uint32_t seq1, std::uint32_t seq2) { - int const diff = std::max(seq1, seq2) - std::min(seq1, seq2); + std::uint32_t const diff = std::max(seq1, seq2) - std::min(seq1, seq2); if (diff < Tuning::kConvergedLedgerLimit) { @@ -2475,63 +2457,63 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - protocol::TMGetObjectByHash reply; - - reply.set_query(false); - - reply.set_type(packet.type()); - if (packet.has_ledgerhash()) { if (!stringIsUInt256Sized(packet.ledgerhash())) { - fee_.update(Resource::kFeeMalformedRequest, "ledger hash"); + JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_; + fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash"); return; } - - reply.set_ledgerhash(packet.ledgerhash()); } - - fee_.update(Resource::kFeeModerateBurdenPeer, " received a get object by hash request"); - - // This is a very minimal implementation - for (int i = 0; i < packet.objects_size(); ++i) + // Reject oversized requests before touching the NodeStore. + // The legitimate upper bound (InboundLedger::getNeededHashes()) + // is 8 hashes; anything beyond kHardMaxReplyNodes is non-conforming. + if (packet.objects_size() > Tuning::kHardMaxReplyNodes) { - auto const& obj = packet.objects(i); - if (obj.has_hash() && stringIsUInt256Sized(obj.hash())) - { - uint256 const hash = uint256::fromRaw(obj.hash()); - // VFALCO TODO Move this someplace more sensible so we dont - // need to inject the NodeStore interfaces. - std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; - auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)}; - if (nodeObject) - { - protocol::TMIndexedObject& newObj = *reply.add_objects(); - newObj.set_hash(hash.begin(), hash.size()); - newObj.set_data(&nodeObject->getData().front(), nodeObject->getData().size()); - - if (obj.has_nodeid()) - newObj.set_index(obj.nodeid()); - if (obj.has_ledgerseq()) - newObj.set_ledgerseq(obj.ledgerseq()); - - // Check if by adding this object, reply has reached its - // limit - if (reply.objects_size() >= Tuning::kHardMaxReplyNodes) - { - fee_.update( - Resource::kFeeModerateBurdenPeer, - "Reply limit reached. Truncating reply."); - break; - } - } - } + JLOG(pJournal_.warn()) + << "GetObj: oversized request from peer " << id_ << " (" << packet.objects_size() + << " > " << Tuning::kHardMaxReplyNodes << ")"; + fee_.update(Resource::kFeeInvalidData, "oversized get object request"); + return; } - JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " - << packet.objects_size(); - send(std::make_shared(reply, protocol::mtGET_OBJECTS)); + // Dispatch heavy synchronous NodeStore lookups off the peer's + // I/O strand and onto the bounded job queue, mirroring the pattern + // used by processLedgerRequest. + std::weak_ptr const weak = shared_from_this(); + bool const queued = app_.getJobQueue().addJob(JtLedgerReq, "RcvGetObjByHash", [weak, m]() { + auto peer = weak.lock(); + if (!peer) + return; + try + { + peer->processGetObjectByHash(m); + } + catch (std::exception const& e) + { + // Surface backend failures (NodeStore I/O, allocation) + // back through the resource model so a misbehaving peer + // is still accountable rather than silently dropped. + JLOG(peer->pJournal_.warn()) << "GetObj: handler threw: " << e.what(); + peer->charge(Resource::kFeeRequestNoReply, "get object handler exception"); + } + }); + if (!queued) + { + // The JobQueue is no longer accepting new work (typically + // because it is shutting down / has been joined). + JLOG(pJournal_.warn()) << "GetObj: job queue refused request from peer " << id_; + return; + } + + // Admission-time charge: a peer that floods enqueues would + // otherwise be billed only the trivial onMessageEnd fee per + // message until the JobQueue catches up, re-creating an + // uncharged DoS window. Charge the base burden up-front (after + // a successful enqueue); the per-lookup differential is added + // in the worker. + fee_.update(Resource::kFeeModerateBurdenPeer, "received a get object by hash request"); } else { @@ -2587,6 +2569,69 @@ PeerImp::onMessage(std::shared_ptr const& m) } } +void +PeerImp::processGetObjectByHash(std::shared_ptr const& m) +{ + protocol::TMGetObjectByHash const& packet = *m; + + protocol::TMGetObjectByHash reply; + reply.set_query(false); + reply.set_type(packet.type()); + + if (packet.has_ledgerhash()) + { + reply.set_ledgerhash(packet.ledgerhash()); + } + + // Defense in depth: caller (onMessage) already validates cheap + // structural properties of the request before dispatching here: + // - objects_size() <= kHardMaxReplyNodes (oversize gate) + // - if has_ledgerhash() then ledgerhash is uint256-sized + // The iteration cap below mirrors the oversize gate so this method + // remains safe if invoked directly by tests or future callers, and + // a peer cannot drive unbounded NodeStore lookups by sending + // non-existent hashes. + int const requested = packet.objects_size(); + int const iterLimit = std::min(requested, Tuning::kHardMaxReplyNodes); + + for (int i = 0; i < iterLimit; ++i) + { + auto const& obj = packet.objects(i); + if (!obj.has_hash() || !stringIsUInt256Sized(obj.hash())) + continue; + + uint256 const hash = uint256::fromRaw(obj.hash()); + // VFALCO TODO Move this someplace more sensible so we don't + // need to inject the NodeStore interfaces. + std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; + auto const nodeObject = app_.getNodeStore().fetchNodeObject(hash, seq); + if (!nodeObject) + continue; + + protocol::TMIndexedObject& newObj = *reply.add_objects(); + newObj.set_hash(hash.begin(), hash.size()); + auto const& data = nodeObject->getData(); + newObj.set_data(data.data(), data.size()); + if (obj.has_nodeid()) + newObj.set_index(obj.nodeid()); + if (obj.has_ledgerseq()) + newObj.set_ledgerseq(obj.ledgerseq()); + } + + // Apply work-proportional charge. `charge()` posts the disconnect + // step (if any) back to strand_, so it is safe to call from this + // JobQueue worker thread. + charge( + // We pass `requested` directly here, instead of actual lookups done. Which could be + // std::min(packet.objects_size(), static_cast(Tuning::kHardMaxReplyNodes)); + // Because we want to charge as per the request size, to discourage large requests. + computeGetObjectByHashFee(requested, reply.objects_size()), + "processed get object by hash request"); + + JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " << requested; + send(std::make_shared(reply, protocol::mtGET_OBJECTS)); +} + void PeerImp::onMessage(std::shared_ptr const& m) { @@ -2677,45 +2722,42 @@ PeerImp::onMessage(std::shared_ptr const& m) void PeerImp::onMessage(std::shared_ptr const& m) { - using on_message_fn = void (PeerImp::*)(std::shared_ptr const&); - if (!strand_.running_in_this_thread()) - { - post(strand_, std::bind((on_message_fn)&PeerImp::onMessage, shared_from_this(), m)); - return; - } + dispatch(strand_, [self = shared_from_this(), m]() { + if (!m->has_validatorpubkey()) + { + self->fee_.update(Resource::kFeeInvalidData, "squelch no pubkey"); + return; + } + auto validator = m->validatorpubkey(); + auto const slice{makeSlice(validator)}; + if (!publicKeyType(slice)) + { + self->fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey"); + return; + } + PublicKey const key(slice); - if (!m->has_validatorpubkey()) - { - fee_.update(Resource::kFeeInvalidData, "squelch no pubkey"); - return; - } - auto validator = m->validatorpubkey(); - auto const slice{makeSlice(validator)}; - if (!publicKeyType(slice)) - { - fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey"); - return; - } - PublicKey const key(slice); + // Ignore the squelch for validator's own messages. + if (key == self->app_.getValidationPublicKey()) + { + JLOG(self->pJournal_.debug()) + << "onMessage: TMSquelch discarding validator's squelch " << slice; + return; + } - // Ignore the squelch for validator's own messages. - if (key == app_.getValidationPublicKey()) - { - JLOG(pJournal_.debug()) << "onMessage: TMSquelch discarding validator's squelch " << slice; - return; - } + std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0; + if (!m->squelch()) + { + self->squelch_.removeSquelch(key); + } + else if (!self->squelch_.addSquelch(key, std::chrono::seconds{duration})) + { + self->fee_.update(Resource::kFeeInvalidData, "squelch duration"); + } - std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0; - if (!m->squelch()) - { - squelch_.removeSquelch(key); - } - else if (!squelch_.addSquelch(key, std::chrono::seconds{duration})) - { - fee_.update(Resource::kFeeInvalidData, "squelch duration"); - } - - JLOG(pJournal_.debug()) << "onMessage: TMSquelch " << slice << " " << id() << " " << duration; + JLOG(self->pJournal_.debug()) + << "onMessage: TMSquelch " << slice << " " << self->id() << " " << duration; + }); } //-------------------------------------------------------------------------- @@ -3414,6 +3456,53 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) send(std::make_shared(ledgerData, protocol::mtLEDGER_DATA)); } +// Differential pricing helper. Returns only the *dynamic* component +// of the per-message charge — the base `kFeeModerateBurdenPeer` is +// applied at admission time in `onMessage(TMGetObjectByHash)` so a +// high traffic client pays for the message regardless of when (or +// whether) the worker runs. +// +// Dynamic charge model: +// +// billable = max(0, requested - kFreeObjectsPerRequest) +// missed = max(0, requested - found) +// billableMisses = min(missed, billable) // misses billed first +// billableHits = billable - billableMisses +// sizeBand = (requested > kBandMediumMax) ? kCostBandLarge +// : (requested > kBandSmallMax) ? kCostBandMedium +// : kCostBandSmall +// dynamic = billableHits * kCostPerLookupHit +// + billableMisses * kCostPerLookupMiss +// + sizeBand +// +// Misses are billed first against the billable budget because a node store +// seek dominates a cache hit and because invalid hashes are ~100% miss by construction. +Resource::Charge +PeerImp::computeGetObjectByHashFee(int const requested, int const found) +{ + int const billable = std::max(0, requested - static_cast(Tuning::kFreeObjectsPerRequest)); + // Clamp `missed` so a future caller passing found > requested cannot + // produce a negative value that flips the hits/misses split. + int const missed = std::max(0, requested - found); + int const billableMisses = std::min(missed, billable); + int const billableHits = billable - billableMisses; + + int sizeBand = Tuning::kCostBandSmall; + if (requested > Tuning::kBandMediumMax) + { + sizeBand = Tuning::kCostBandLarge; + } + else if (requested > Tuning::kBandSmallMax) + { + sizeBand = Tuning::kCostBandMedium; + } + + int const dynamic = (billableHits * Tuning::kCostPerLookupHit) + + (billableMisses * Tuning::kCostPerLookupMiss) + sizeBand; + + return Resource::Charge(dynamic, "GetObject differential"); +} + int PeerImp::getScore(bool haveItem) const { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index f5d87371be..26d7e0a832 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -147,6 +147,12 @@ private: protocol::TMStatusChange lastStatus_; Resource::Consumer usage_; ChargeWithContext fee_; + + // One-shot guard so concurrent JobQueue workers cannot double-count + // the per-connection peer-disconnect-by-charge metric (and cannot + // post duplicate fail() calls) when several queued requests cross + // kDropThreshold before the first fail() lands on the strand. + std::atomic chargeDisconnectFired_{false}; std::shared_ptr const slot_; boost::beast::multi_buffer readBuffer_; http_request_type request_; @@ -624,6 +630,67 @@ private: void processLedgerRequest(std::shared_ptr const& m); + +protected: + // Kept `protected` so test subclasses (see + // TMGetObjectByHash_test) can drive the + // synchronous processor and the differential-pricing helper without + // routing through the JobQueue or going through `friend` plumbing. + // Production callers reach these members only via + // `onMessage(TMGetObjectByHash)` → JobQueue → `processGetObjectByHash`. + + /** Process a generic-query TMGetObjectByHash message. + + Dispatched from `onMessage(TMGetObjectByHash)` to the JobQueue + (`JtLedgerReq`) so synchronous NodeStore lookups do not block the + peer's I/O strand. Caps iteration at `Tuning::kHardMaxReplyNodes` + regardless of hit/miss outcome and applies differential pricing + via `computeGetObjectByHashFee()` after the fetch loop completes. + + @param m The protocol message containing requested object hashes. + */ + void + processGetObjectByHash(std::shared_ptr const& m); + + /** Compute the per-message resource charge for a TMGetObjectByHash + request based on how much work was actually performed. + + The charge has three components on top of the base + `Resource::kFeeModerateBurdenPeer`: + - per-hit lookup cost (cheap; usually served from cache) + - per-miss lookup cost (expensive node store seeks) + - request-size band surcharge (escalates abusive batch sizes) + + The first `Tuning::kFreeObjectsPerRequest` objects are free so + that legitimate `InboundLedger::getNeededHashes()` traffic + (at most 8 objects) is unaffected. + + @param requested Number of objects requested by the message. This + value is used for request-size pricing and may + exceed `Tuning::kHardMaxReplyNodes` when this + helper is called directly, even though processing + caps the iterations to `Tuning::kHardMaxReplyNodes`. + @param found Number of objects successfully returned in the + reply. + @return A `Resource::Charge` whose cost reflects the work performed. + */ + static Resource::Charge + computeGetObjectByHashFee(int const requested, int const found); + + /** Read-only accessor for the accumulated peer-message charge. + + Exposed at `protected` scope so test subclasses can verify the + oversized-request rejection path (Layer 1) without invoking the + full JobQueue handler. Production callers should never read this back — + the value is consumed by `charge()`/`disconnect()` internally. + + @return The current `Resource::Charge` accumulated on `fee_`. + */ + Resource::Charge + currentFeeCharge() const + { + return fee_.fee; + } }; //------------------------------------------------------------------------------ diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index a0f57ec3d7..20a60d470e 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -1,14 +1,18 @@ #pragma once +#include + +#include +#include namespace xrpl::Tuning { /** How many ledgers off a server can be and we will still consider it converged */ -static constexpr auto kConvergedLedgerLimit = 24; +static constexpr std::uint32_t kConvergedLedgerLimit = 24; /** How many ledgers off a server has to be before we consider it diverged */ -static constexpr auto kDivergedLedgerLimit = 128; +static constexpr std::uint32_t kDivergedLedgerLimit = 128; /** The soft cap on the number of ledger entries in a single reply. */ static constexpr auto kSoftMaxReplyNodes = 8192; @@ -37,4 +41,92 @@ static constexpr auto kMaxQueryDepth = 3; /** Size of buffer used to read from the socket. */ constexpr std::size_t kReadBufferBytes = 16384; +/** TMGetObjectByHash differential pricing. + + Honest peers ask for at most 8 hashes per call (the header, or up to + 4 state + 4 tx hashes from `InboundLedger::getNeededHashes()`). The + free tier covers them at zero cost. Beyond that, each lookup is billed: + 'misses' cost much more than 'hits' because a miss does a node store seek + while a hit is usually served from cache. On top of that, a size-band + surcharge kicks in for larger requests so an attacker who crams a + single message with thousands of hashes blows past + `Resource::kDropThreshold` and gets disconnected. + + The numbers below are picked to keep three things true given + `kDropThreshold = 25000`: + + - Honest traffic (<= 8 objects per request) is free. + - A single all-miss request at `kHardMaxReplyNodes` (12288) costs + more than the drop threshold, so an attacker gets dropped in one + message. + - A peer spamming 1024-object hit-only requests gets dropped in + ~19 messages — fast enough to be useful, slow enough that an + honest peer momentarily sending oversized requests has time to + back off. */ + +/** How many objects a request can ask for before per-lookup billing + begins? + Twice the honest peak (8) so a peer that occasionally retries a hash + never trips pricing. Same value as `SHAMapInnerNode::kBranchFactor`; + that's a coincidence, not a requirement. */ +static constexpr auto kFreeObjectsPerRequest = 16; + +/** Cost of one cache-hit lookup. The unit; everything else is a + multiple of this. */ +static constexpr auto kCostPerLookupHit = 1; + +/** Cost of one node-store miss, in units of `kCostPerLookupHit`. + + A miss does a node store disk seek; a hit usually comes from cache. + The 8x ratio is an order-of-magnitude guess at the latency gap on + SSD-backed nodes, not a measured number. The math only requires this + to be at least 2 — any smaller and a full-miss request at the hard + cap wouldn't trip the drop threshold. 8 leaves headroom: if + `kDropThreshold` goes up or `kHardMaxReplyNodes` comes down, the + drop-on-attack property still holds without a code change. */ +static constexpr auto kCostPerLookupMiss = 8; + +/** Size-band surcharges. Whichever band a request's size falls into, + its surcharge is added once on top of the per-lookup cost. + + The job of the surcharge is to make crossing a band edge feel like + a step, not a slope. With these values, the cost roughly doubles or triples at each cliff: + + n=64: costs 48 => n=65 costs 149 (~3x jump) + n=1024: costs 1108 => n=1025 costs 2009 (~2x jump) + + The 10x step between medium and large mirrors the ~16x step + between the band edges (64 -> 1024) so the cliff feels comparable + at both scales. + */ +static constexpr auto kCostBandSmall = 0; +static constexpr auto kCostBandMedium = 100; +static constexpr auto kCostBandLarge = 1000; + +/** How many hashes per type an honest peer asks for at a time. + + Matches the `4` passed to `neededStateHashes(4)` and + `neededTxHashes(4)` in `InboundLedger::getNeededHashes()`. Kept here + instead of imported from the ledger module so overlay stays + self-contained; if that `4` ever changes, update this in lockstep or + the band thresholds below will start charging honest peers. */ +static constexpr auto kLegitHashesPerType = 4; + +/** Cutoffs that decide which size band a request falls into. + + A SHAMap inner node has 16 children; an honest peer asks for 4 + hashes per type. So: + + kBandSmallMax = 4 * 16 = 64 // one inner node's worth + kBandMediumMax = 4 * 16^2 = 1024 // a depth-2 subtree's worth + + A request up to 64 objects is small (no surcharge); up to 1024 is + medium; anything larger is large. The bounds are inclusive: a + request of exactly 64 is small, 65 is medium. Anything past 1024 is + well beyond what the honest sync path produces, so it's billed at + the large rate to drive attack-shaped traffic over the drop + threshold quickly. */ +static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor; +static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor; + } // namespace xrpl::Tuning diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 8c3a5ea245..b8731a2289 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -1256,7 +1256,7 @@ transactionSignFor( // The array must be sorted and validated. // For delegated transactions, the delegate account is // the one forbidden from appearing in its own Signers array. - auto err = sortAndValidateSigners(signers, sttx->getFeePayer()); + auto err = sortAndValidateSigners(signers, sttx->getInitiator()); if (RPC::containsError(err)) return err; } @@ -1423,9 +1423,9 @@ transactionSubmitMultiSigned( } // The array must be sorted and validated. - // For delegated transactions, getFeePayer() returns sfDelegate, + // For delegated transactions, getInitiator() returns sfDelegate, // that account is the one forbidden from appearing in its own Signers array. - auto err = sortAndValidateSigners(signers, stTx->getFeePayer()); + auto err = sortAndValidateSigners(signers, stTx->getInitiator()); if (RPC::containsError(err)) return err; diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index a51737a392..3e1a29efa2 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -33,7 +33,8 @@ namespace xrpl { @param dirIndex Begin gathering account objects from this directory. @param entryIndex Begin gathering objects from this directory node. @param limit Maximum number of objects to find. - @param sponsored Whether to filter by sponsored objects. + @param hasSponsoredFilter Whether to filter by sponsored objects. + @param sponsored Whether filtered objects should be sponsored. @param jvResult A JSON result that holds the request objects. */ bool @@ -44,7 +45,8 @@ getAccountObjects( uint256 dirIndex, uint256 entryIndex, std::uint32_t const limit, - std::optional const sponsored, + bool const hasSponsoredFilter, + bool const sponsored, json::Value& jvResult) { // check if dirIndex is valid @@ -103,12 +105,12 @@ getAccountObjects( while (currentPage) { bool canAppendNFT = true; - if (sponsored.has_value()) + if (hasSponsoredFilter) { std::optional const nftSponsor = currentPage->isFieldPresent(sfSponsor) ? currentPage->getAccountID(sfSponsor) : std::optional(std::nullopt); - if (!sponsoredMatchesFilter(sponsored.value(), nftSponsor)) + if (!sponsoredMatchesFilter(sponsored, nftSponsor)) canAppendNFT = false; } if (canAppendNFT) @@ -204,21 +206,33 @@ getAccountObjects( !typeMatchesFilter(typeFilter.value(), sleNode->getType())) canAppend = false; - auto const getSponsor = [&sleNode]() -> std::optional { - if (sleNode->isFieldPresent(sfSponsor)) - return sleNode->getAccountID(sfSponsor); + auto const getSponsor = [&account, &sleNode]() -> std::optional { if (sleNode->getType() == ltRIPPLE_STATE) { - if (sleNode->isFieldPresent(sfHighSponsor)) + if (sleNode->isFlag(lsfHighReserve) && + sleNode->getFieldAmount(sfHighLimit).getIssuer() == account && + sleNode->isFieldPresent(sfHighSponsor)) return sleNode->getAccountID(sfHighSponsor); - if (sleNode->isFieldPresent(sfLowSponsor)) + if (sleNode->isFlag(lsfLowReserve) && + sleNode->getFieldAmount(sfLowLimit).getIssuer() == account && + sleNode->isFieldPresent(sfLowSponsor)) return sleNode->getAccountID(sfLowSponsor); + + return std::nullopt; } + + if (sleNode->getType() == ltSPONSORSHIP && + sleNode->getAccountID(sfOwner) != account) + return std::nullopt; + + if (sleNode->isFieldPresent(sfSponsor)) + return sleNode->getAccountID(sfSponsor); + return std::nullopt; }; std::optional const sponsor = getSponsor(); - if (sponsored.has_value() && !sponsoredMatchesFilter(sponsored.value(), sponsor)) + if (hasSponsoredFilter && !sponsoredMatchesFilter(sponsored, sponsor)) canAppend = false; if (canAppend) @@ -369,8 +383,9 @@ doAccountObjects(RPC::JsonContext& context) return RPC::invalidFieldError(jss::marker); } - std::optional sponsored; - if (params.isMember(jss::sponsored)) + bool const hasSponsoredFilter = params.isMember(jss::sponsored); + bool sponsored = false; + if (hasSponsoredFilter) { auto const& sponsoredJv = params[jss::sponsored]; if (!sponsoredJv.isBool()) @@ -380,7 +395,15 @@ doAccountObjects(RPC::JsonContext& context) } if (!getAccountObjects( - *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsored, result)) + *ledger, + accountID, + typeFilter, + dirIndex, + entryIndex, + limit, + hasSponsoredFilter, + sponsored, + result)) return RPC::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 7cccac8c3e..b4f1a55689 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -719,6 +719,30 @@ parseSignerList( return parseObjectID(params, fieldName, "hex string"); } +static std::expected +parseSponsorship( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const sponsorID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); + if (!sponsorID) + return std::unexpected(sponsorID.error()); + + auto const sponseeID = + LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); + if (!sponseeID) + return std::unexpected(sponseeID.error()); + + return keylet::sponsorship(*sponsorID, *sponseeID).key; +} + static std::expected parseTicket( json::Value const& params, @@ -764,30 +788,6 @@ parseVault( return keylet::vault(*id, *seq).key; } -static std::expected -parseSponsorship( - json::Value const& params, - json::StaticString const fieldName, - [[maybe_unused]] unsigned const apiVersion) -{ - if (!params.isObject()) - { - return parseObjectID(params, fieldName); - } - - auto const sponsorAccountID = - LedgerEntryHelpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); - if (!sponsorAccountID) - return std::unexpected(sponsorAccountID.error()); - - auto const sponseeAccountID = - LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); - if (!sponseeAccountID) - return std::unexpected(sponseeAccountID.error()); - - return keylet::sponsor(*sponsorAccountID, *sponseeAccountID).key; -} - static std::expected parseXChainOwnedClaimID( json::Value const& claimId, diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp index 36dae615b3..af42af2a55 100644 --- a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp @@ -186,13 +186,23 @@ doUnsubscribe(RPC::JsonContext& context) book.domain = domain; } - context.netOps.unsubBook(ispSub->getSeq(), book); + if (!context.netOps.unsubBook(ispSub, book)) + { + JLOG(context.j.debug()) + << "doUnsubscribe: book not subscribed (no-op for seq=" << ispSub->getSeq() + << ")"; + } // both_sides is deprecated. if ((jv.isMember(jss::both) && jv[jss::both].asBool()) || (jv.isMember(jss::both_sides) && jv[jss::both_sides].asBool())) { - context.netOps.unsubBook(ispSub->getSeq(), reversed(book)); + if (!context.netOps.unsubBook(ispSub, reversed(book))) + { + JLOG(context.j.debug()) + << "doUnsubscribe: reversed book not subscribed (no-op for seq=" + << ispSub->getSeq() << ")"; + } } } } diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index e0bb8d8a9f..82c77adb3b 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -131,29 +131,16 @@ autofillSignature(json::Value& sigObject) static std::optional autofillTx(json::Value& txJson, RPC::JsonContext& context) { - if (!txJson.isMember(jss::Fee)) - { - // autofill Fee - // Must happen after all the other autofills happen - // Error handling/messaging works better that way - auto feeOrError = RPC::getCurrentNetworkFee( - context.role, - context.app.config(), - context.app.getFeeTrack(), - context.app.getTxQ(), - context.app, - txJson); - if (feeOrError.isMember(jss::error)) - return feeOrError; - txJson[jss::Fee] = feeOrError; - } - if (auto error = autofillSignature(txJson)) return error; if (txJson.isMember(sfSponsorSignature.jsonName)) { - if (auto error = autofillSignature(txJson[sfSponsorSignature.jsonName])) + auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; + if (!sponsorSignature.isObject()) + return RPC::objectFieldError(sfSponsorSignature.jsonName); + + if (auto const error = autofillSignature(sponsorSignature)) return error; } @@ -172,6 +159,22 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) txJson[jss::NetworkID] = to_string(networkId); } + if (!txJson.isMember(jss::Fee)) + { + // Autofill Fee after normalizing nested signer fields so the fee + // estimator sees the full transaction shape. + auto feeOrError = RPC::getCurrentNetworkFee( + context.role, + context.app.config(), + context.app.getFeeTrack(), + context.app.getTxQ(), + context.app, + txJson); + if (feeOrError.isMember(jss::error)) + return feeOrError; + txJson[jss::Fee] = feeOrError; + } + return std::nullopt; }